Merge pull request #465 from CommunityToolkit/user/sergiopedri/extensions-dependencyinjection

[Experiment] CommunityToolkit.Extensions.DependencyInjection package
This commit is contained in:
Arlo 2023-07-17 13:07:36 -05:00 коммит произвёл GitHub
Родитель 26998e9e17 1e29391ad4
Коммит 3670f485c0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
31 изменённых файлов: 1988 добавлений и 2 удалений

2
.github/workflows/build.yml поставляемый
Просмотреть файл

@ -16,7 +16,7 @@ on:
workflow_dispatch:
env:
DOTNET_VERSION: ${{ '6.0.x' }}
DOTNET_VERSION: ${{ '7.0.x' }}
ENABLE_DIAGNOSTICS: false
#COREHOST_TRACE: 1
COREHOST_TRACEFILE: corehosttrace.log

Просмотреть файл

@ -0,0 +1,13 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
## Release 1.0
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TKEXDI0001 | CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.InvalidServiceRegistrationAnalyzer | Error |
TKEXDI0002 | CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.InvalidServiceRegistrationAnalyzer | Error |
TKEXDI0003 | CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.InvalidServiceRegistrationAnalyzer | Error |
TKEXDI0004 | CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.InvalidServiceRegistrationAnalyzer | Warning |

Просмотреть файл

@ -0,0 +1,2 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

Просмотреть файл

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!--
Suppress ref safety warnings in unsafe contexts (see https://github.com/dotnet/csharplang/issues/6476).
This is used eg. to replace Unsafe.SizeOf<T>() calls with just sizeof(T), or to just use raw pointers to
reinterpret references to managed objects when it is safe to do so. The warnings are not necessary in this
context, since in order to use these APIs the caller already has to be in an unsafe context.
-->
<NoWarn>$(NoWarn);CS8500</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" Pack="false" />
<PackageReference Include="PolySharp" Version="1.13.2" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
<!-- Remove imported global usings -->
<ItemGroup>
<Compile Remove="$(ToolingDirectory)\GlobalUsings.cs" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,151 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
using static CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Diagnostics.DiagnosticDescriptors;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators;
/// <summary>
/// A diagnostic analyzer that emits diagnostics for invalid service registrations.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidServiceRegistrationAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// The mapping of target attributes that will trigger the analyzer.
/// </summary>
private static readonly ImmutableDictionary<string, string> RegistrationAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[]
{
new KeyValuePair<string, string>("SingletonAttribute", "CommunityToolkit.Extensions.DependencyInjection.SingletonAttribute"),
new KeyValuePair<string, string>("TransientAttribute", "CommunityToolkit.Extensions.DependencyInjection.TransientAttribute"),
});
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
InvalidRegistrationImplementationType,
InvalidRegistrationServiceType,
DuplicateImplementationTypeRegistration,
DuplicateServiceTypeRegistration);
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(static context =>
{
// Try to get all necessary type symbols
if (!context.Compilation.TryBuildNamedTypeSymbolMap(RegistrationAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary<string, INamedTypeSymbol>? typeSymbols))
{
return;
}
// Register a callback for all methpds
context.RegisterSymbolAction(context =>
{
IMethodSymbol methodSymbol = (IMethodSymbol)context.Symbol;
HashSet<ISymbol> implementationTypeRegistrations = new(SymbolEqualityComparer.Default);
HashSet<ISymbol> serviceTypeRegistrations = new(SymbolEqualityComparer.Default);
foreach (AttributeData attributeData in context.Symbol.GetAttributes())
{
// Go over each attribute on the target method and find the ones indicating a service registration
if (attributeData.AttributeClass is { Name: string attributeName } attributeClass &&
typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? attributeSymbol) &&
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
{
// Ensure the attribute arguments are present, and retrieve them
if (attributeData.ConstructorArguments is not [
{ Kind: TypedConstantKind.Type, Value: ITypeSymbol implementationType },
{ Kind: TypedConstantKind.Array, Values: ImmutableArray<TypedConstant> serviceTypes }])
{
continue;
}
// Check the implementation type is valid (note: not checking constructors just yet)
if (implementationType is not INamedTypeSymbol
{
TypeKind: TypeKind.Class,
IsStatic: false,
IsAbstract: false,
InstanceConstructors: [IMethodSymbol, ..] constructors
})
{
// The type is not valid, emit a diagnostic
context.ReportDiagnostic(Diagnostic.Create(
InvalidRegistrationImplementationType,
attributeData.GetLocation(),
implementationType));
}
// Check if the implementation type has not been seen before
if (!implementationTypeRegistrations.Add(implementationType))
{
context.ReportDiagnostic(Diagnostic.Create(
DuplicateImplementationTypeRegistration,
attributeData.GetLocation(),
implementationType));
}
// If no service types are present, the service is registered as the implementation type
if (serviceTypes.IsEmpty)
{
// Since we're tracking all registered service types, we need to track that case as well.
// This covers cases such as:
//
// [Singleton(typeof(A))]
// [Singleton(typeof(BDerivesFromA), typeof(A), typeof(IB))]
//
// That is, the first attribute will trigger this code path and the implementation type
// will be registered, and the second attribute will go through the explicit list of
// service types to register, see A being present again, and correctly emit the diagnostic.
if (!serviceTypeRegistrations.Add(implementationType))
{
context.ReportDiagnostic(Diagnostic.Create(
DuplicateServiceTypeRegistration,
attributeData.GetLocation(),
implementationType));
}
}
else
{
// Go over all declared service types and validate them as well
foreach (TypedConstant serviceType in serviceTypes)
{
// For a service type to be valid, there has to be an implicit or identity conversion between that and the service type
if (serviceType.Value is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Interface, IsStatic: false } targetServiceType ||
context.Compilation.ClassifyCommonConversion(implementationType, targetServiceType) is not ({ IsIdentity: true } or { IsImplicit: true }))
{
// The service type is not valid, emit a diagnostic
context.ReportDiagnostic(Diagnostic.Create(
InvalidRegistrationServiceType,
attributeData.GetLocation(),
implementationType,
serviceType.Value));
}
// Check if the service type has not been seen before
if (serviceType.Value is ITypeSymbol typeSymbol &&
!serviceTypeRegistrations.Add(typeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
DuplicateServiceTypeRegistration,
attributeData.GetLocation(),
typeSymbol));
}
}
}
}
}
}, SymbolKind.Method);
});
}
}

Просмотреть файл

@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
#pragma warning disable IDE0090 // Use 'new(...)' for field initializers, suppressed as it breaks a Roslyn analyzer
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Diagnostics;
/// <summary>
/// A container for all <see cref="DiagnosticDescriptor"/> instances for errors reported by analyzers in this project.
/// </summary>
internal static class DiagnosticDescriptors
{
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a registered service is using an invalid implementation type.
/// <para>
/// Format: <c>"Cannot register a service of implementation type {0}, as the type has to be a non static, non abstract class with a public constructor"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidRegistrationImplementationType = new DiagnosticDescriptor(
id: "TKEXDI0001",
title: "Invalid registration implementation type",
messageFormat: "Cannot register a service of implementation type {0}, as the type has to be a non static, non abstract class with a public constructor",
category: typeof(InvalidServiceRegistrationAnalyzer).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Registered service implementation types must be non static, non abstract classes with a public constructor.");
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a registered service is using an invalid service type.
/// <para>
/// Format: <c>"Cannot register a service of implementation type {0} with the type {1}, as there is no implicit type conversion between the two"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidRegistrationServiceType = new DiagnosticDescriptor(
id: "TKEXDI0002",
title: "Invalid registration service type",
messageFormat: "Cannot register a service of implementation type {0} with the type {1}, as there is no implicit type conversion between the two",
category: typeof(InvalidServiceRegistrationAnalyzer).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Registered service types must be implicitly convertible from their implementation type.");
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when an implementation type is registered twice.
/// <para>
/// Format: <c>"The implementation type {0} has already been registered on the target service collection"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor DuplicateImplementationTypeRegistration = new DiagnosticDescriptor(
id: "TKEXDI0003",
title: "Duplicate implementation type registration",
messageFormat: "The implementation type {0} has already been registered on the target service collection",
category: typeof(InvalidServiceRegistrationAnalyzer).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Each implementation type can only be registered once in a target service collection.");
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a service type is registered twice.
/// <para>
/// Format: <c>"The service type {0} has already been registered on the target service collection"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor DuplicateServiceTypeRegistration = new DiagnosticDescriptor(
id: "TKEXDI0004",
title: "Duplicate service type registration",
messageFormat: "The service type {0} has already been registered on the target service collection",
category: typeof(InvalidServiceRegistrationAnalyzer).FullName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Each service type should only be registered once in a target service collection.");
}

Просмотреть файл

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
/// <summary>
/// Extension methods for the <see cref="AttributeData"/> type.
/// </summary>
internal static class AttributeDataExtensions
{
/// <summary>
/// Tries to get the location of the input <see cref="AttributeData"/> instance.
/// </summary>
/// <param name="attributeData">The input <see cref="AttributeData"/> instance to get the location for.</param>
/// <returns>The resulting location for <paramref name="attributeData"/>, if a syntax reference is available.</returns>
public static Location? GetLocation(this AttributeData attributeData)
{
if (attributeData.ApplicationSyntaxReference is { } syntaxReference)
{
return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span);
}
return null;
}
}

Просмотреть файл

@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
/// <summary>
/// Extension methods for the <see cref="Compilation"/> type.
/// </summary>
internal static class CompilationExtensions
{
/// <summary>
/// Tries to build a map of <see cref="INamedTypeSymbol"/> instances form the input mapping of names.
/// </summary>
/// <typeparam name="T">The type of keys for each symbol.</typeparam>
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
/// <param name="typeNames">The input mapping of <typeparamref name="T"/> keys to fully qualified type names.</param>
/// <param name="typeSymbols">The resulting mapping of <typeparamref name="T"/> keys to resolved <see cref="INamedTypeSymbol"/> instances.</param>
/// <returns>Whether all requested <see cref="INamedTypeSymbol"/> instances could be resolved.</returns>
public static bool TryBuildNamedTypeSymbolMap<T>(
this Compilation compilation,
IEnumerable<KeyValuePair<T, string>> typeNames,
[NotNullWhen(true)] out ImmutableDictionary<T, INamedTypeSymbol>? typeSymbols)
where T : IEquatable<T>
{
ImmutableDictionary<T, INamedTypeSymbol>.Builder builder = ImmutableDictionary.CreateBuilder<T, INamedTypeSymbol>();
foreach (KeyValuePair<T, string> pair in typeNames)
{
if (compilation.GetTypeByMetadataName(pair.Value) is not INamedTypeSymbol attributeSymbol)
{
typeSymbols = null;
return false;
}
builder.Add(pair.Key, attributeSymbol);
}
typeSymbols = builder.ToImmutable();
return true;
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
/// <summary>
/// Extension methods for the <see cref="ISymbol"/> type.
/// </summary>
internal static class ISymbolExtensions
{
/// <summary>
/// Gets the fully qualified name for a given symbol.
/// </summary>
/// <param name="symbol">The input <see cref="ISymbol"/> instance.</param>
/// <returns>The fully qualified name for <paramref name="symbol"/>.</returns>
public static string GetFullyQualifiedName(this ISymbol symbol)
{
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
}

Просмотреть файл

@ -0,0 +1,93 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.CodeAnalysis;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
/// <summary>
/// Extension methods for the <see cref="ITypeSymbol"/> type.
/// </summary>
internal static class ITypeSymbolExtensions
{
/// <summary>
/// Checks whether or not a given type symbol has a specified fully qualified metadata name.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="name">The full name to check.</param>
/// <returns>Whether <paramref name="symbol"/> has a full name equals to <paramref name="name"/>.</returns>
public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name)
{
using ImmutableArrayBuilder<char> builder = ImmutableArrayBuilder<char>.Rent();
symbol.AppendFullyQualifiedMetadataName(in builder);
return builder.WrittenSpan.SequenceEqual(name.AsSpan());
}
/// <summary>
/// Gets the fully qualified metadata name for a given <see cref="ITypeSymbol"/> instance.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
/// <returns>The fully qualified metadata name for <paramref name="symbol"/>.</returns>
public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol)
{
using ImmutableArrayBuilder<char> builder = ImmutableArrayBuilder<char>.Rent();
symbol.AppendFullyQualifiedMetadataName(in builder);
return builder.ToString();
}
/// <summary>
/// Appends the fully qualified metadata name for a given symbol to a target builder.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
/// <param name="builder">The target <see cref="ImmutableArrayBuilder{T}"/> instance.</param>
private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, in ImmutableArrayBuilder<char> builder)
{
static void BuildFrom(ISymbol? symbol, in ImmutableArrayBuilder<char> builder)
{
switch (symbol)
{
// Namespaces that are nested also append a leading '.'
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
BuildFrom(symbol.ContainingNamespace, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Other namespaces (ie. the one right before global) skip the leading '.'
case INamespaceSymbol { IsGlobalNamespace: false }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Types with no namespace just have their metadata name directly written
case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Types with a containing non-global namespace also append a leading '.'
case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }:
BuildFrom(namespaceSymbol, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Nested types append a leading '+'
case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }:
BuildFrom(typeSymbol, in builder);
builder.Add('+');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
default:
break;
}
}
BuildFrom(symbol, in builder);
}
}

Просмотреть файл

@ -0,0 +1,101 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System;
using Microsoft.CodeAnalysis;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
/// <summary>
/// Extension methods for the <see cref="IncrementalValuesProvider{TValues}"/> type.
/// </summary>
internal static class IncrementalValuesProviderExtensions
{
/// <summary>
/// Concatenates two <see cref="IncrementalValuesProvider{TValues}"/> sources into one.
/// </summary>
/// <typeparam name="T">The type of items to combine.</typeparam>
/// <param name="left">The first source.</param>
/// <param name="right">The second source.</param>
/// <returns>The resulting sequence combining items from both <paramref name="left"/> and then <paramref name="right"/>.</returns>
public static IncrementalValuesProvider<T> Concat<T>(this IncrementalValuesProvider<T> left, IncrementalValuesProvider<T> right)
{
IncrementalValueProvider<ImmutableArray<T>> leftItems = left.Collect();
IncrementalValueProvider<ImmutableArray<T>> rightItems = right.Collect();
IncrementalValueProvider<(ImmutableArray<T> Left, ImmutableArray<T> Right)> allItems = leftItems.Combine(rightItems);
return allItems.SelectMany((item, token) =>
{
ImmutableArray<T>.Builder builder = ImmutableArray.CreateBuilder<T>(item.Left.Length + item.Right.Length);
builder.AddRange(item.Left);
builder.AddRange(item.Right);
return builder.MoveToImmutable();
});
}
/// <summary>
/// Groups items in a given <see cref="IncrementalValuesProvider{TValue}"/> sequence by a specified key.
/// </summary>
/// <typeparam name="T">The type of items in the source.</typeparam>
/// <typeparam name="TKey">The type of resulting key elements.</typeparam>
/// <typeparam name="TElement">The type of resulting projected elements.</typeparam>
/// <param name="source">The input <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
/// <param name="keySelector">The key selection <see cref="Func{T, TResult}"/>.</param>
/// <returns>An <see cref="IncrementalValuesProvider{TValues}"/> with the grouped results.</returns>
public static IncrementalValuesProvider<T> GroupBy<T, TKey, TElement>(
this IncrementalValuesProvider<T> source,
Func<T, TKey> keySelector,
Func<T, ImmutableArray<TElement>> elementsSelector,
Func<TKey, ImmutableArray<TElement>, T> resultSelector)
where T : IEquatable<T>
where TKey : IEquatable<TKey>
where TElement : IEquatable<TElement>
{
return source.Collect().SelectMany((item, token) =>
{
Dictionary<TKey, ImmutableArrayBuilder<TElement>> map = new();
// For each input item, extract the key and the items and group them
foreach (T entry in item)
{
TKey key = keySelector(entry);
ImmutableArray<TElement> items = elementsSelector(entry);
// Get or create a builder backed by a pooled array
if (!map.TryGetValue(key, out ImmutableArrayBuilder<TElement> builder))
{
builder = ImmutableArrayBuilder<TElement>.Rent();
map.Add(key, builder);
}
// Aggregate all items for the current key
builder.AddRange(items.AsSpan());
}
token.ThrowIfCancellationRequested();
ImmutableArray<T>.Builder result = ImmutableArray.CreateBuilder<T>(map.Count);
// For each grouped pair, create the resulting item
foreach (KeyValuePair<TKey, ImmutableArrayBuilder<TElement>> entry in map)
{
// Copy the aggregated items to a new array
ImmutableArray<TElement> elements = entry.Value.ToImmutable();
// Manually dispose the rented buffer to return the array to the pool
entry.Value.Dispose();
result.Add(resultSelector(entry.Key, elements));
}
return result;
});
}
}

Просмотреть файл

@ -0,0 +1,177 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
/// <summary>
/// An imutable, equatable array. This is equivalent to <see cref="ImmutableArray{T}"/> but with value equality support.
/// </summary>
/// <typeparam name="T">The type of values in the array.</typeparam>
internal readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private readonly T[]? array;
/// <summary>
/// Creates a new <see cref="EquatableArray{T}"/> instance.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> to wrap.</param>
public EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, T[]?>(ref array);
}
/// <summary>
/// Gets a reference to an item at a specified position within the array.
/// </summary>
/// <param name="index">The index of the item to retrieve a reference to.</param>
/// <returns>A reference to an item at a specified position within the array.</returns>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray()[index];
}
/// <summary>
/// Gets a value indicating whether the current array is empty.
/// </summary>
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().IsEmpty;
}
/// <sinheritdoc/>
public bool Equals(EquatableArray<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}
/// <sinheritdoc/>
public override bool Equals(object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}
/// <sinheritdoc/>
public override unsafe int GetHashCode()
{
if (this.array is not T[] array)
{
return 0;
}
int hashCode = 0;
foreach (T value in array)
{
hashCode = unchecked((hashCode * (int)0xA5555529) + value.GetHashCode());
}
return hashCode;
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}"/> instance from the current <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>The <see cref="ImmutableArray{T}"/> from the current <see cref="EquatableArray{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}
/// <summary>
/// Returns a <see cref="ReadOnlySpan{T}"/> wrapping the current items.
/// </summary>
/// <returns>A <see cref="ReadOnlySpan{T}"/> wrapping the current items.</returns>
public ReadOnlySpan<T> AsSpan()
{
return AsImmutableArray().AsSpan();
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.</returns>
public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
/// <summary>
/// Implicitly converts an <see cref="ImmutableArray{T}"/> to <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}
/// <summary>
/// Implicitly converts an <see cref="EquatableArray{T}"/> to <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}"/> instance from a given <see cref="EquatableArray{T}"/>.</returns>
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are equal.</returns>
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are not the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are not equal.</returns>
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
}

Просмотреть файл

@ -0,0 +1,267 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
/// <summary>
/// A helper type to build sequences of values with pooled buffers.
/// </summary>
/// <typeparam name="T">The type of items to create sequences for.</typeparam>
internal struct ImmutableArrayBuilder<T> : IDisposable
{
/// <summary>
/// The rented <see cref="Writer"/> instance to use.
/// </summary>
private Writer? writer;
/// <summary>
/// Creates a <see cref="ImmutableArrayBuilder{T}"/> value with a pooled underlying data writer.
/// </summary>
/// <returns>A <see cref="ImmutableArrayBuilder{T}"/> instance to write data to.</returns>
public static ImmutableArrayBuilder<T> Rent()
{
return new(new Writer());
}
/// <summary>
/// Creates a new <see cref="ImmutableArrayBuilder{T}"/> object with the specified parameters.
/// </summary>
/// <param name="writer">The target data writer to use.</param>
private ImmutableArrayBuilder(Writer writer)
{
this.writer = writer;
}
/// <inheritdoc cref="ImmutableArray{T}.Builder.Count"/>
public readonly int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.writer!.Count;
}
/// <summary>
/// Gets the data written to the underlying buffer so far, as a <see cref="ReadOnlySpan{T}"/>.
/// </summary>
[UnscopedRef]
public readonly ReadOnlySpan<T> WrittenSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.writer!.WrittenSpan;
}
/// <inheritdoc cref="ImmutableArray{T}.Builder.Add(T)"/>
public readonly void Add(T item)
{
this.writer!.Add(item);
}
/// <summary>
/// Adds the specified items to the end of the array.
/// </summary>
/// <param name="items">The items to add at the end of the array.</param>
public readonly void AddRange(scoped ReadOnlySpan<T> items)
{
this.writer!.AddRange(items);
}
/// <inheritdoc cref="ImmutableArray{T}.Builder.ToImmutable"/>
public readonly ImmutableArray<T> ToImmutable()
{
T[] array = this.writer!.WrittenSpan.ToArray();
return Unsafe.As<T[], ImmutableArray<T>>(ref array);
}
/// <inheritdoc cref="ImmutableArray{T}.Builder.ToArray"/>
public readonly T[] ToArray()
{
return this.writer!.WrittenSpan.ToArray();
}
/// <summary>
/// Gets an <see cref="IEnumerable{T}"/> instance for the current builder.
/// </summary>
/// <returns>An <see cref="IEnumerable{T}"/> instance for the current builder.</returns>
/// <remarks>
/// The builder should not be mutated while an enumerator is in use.
/// </remarks>
public readonly IEnumerable<T> AsEnumerable()
{
return this.writer!;
}
/// <inheritdoc/>
public override readonly string ToString()
{
return this.writer!.WrittenSpan.ToString();
}
/// <inheritdoc cref="IDisposable.Dispose"/>
public void Dispose()
{
Writer? writer = this.writer;
this.writer = null;
writer?.Dispose();
}
/// <summary>
/// A class handling the actual buffer writing.
/// </summary>
private sealed class Writer : ICollection<T>, IDisposable
{
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private T?[]? array;
/// <summary>
/// The starting offset within <see cref="array"/>.
/// </summary>
private int index;
/// <summary>
/// Creates a new <see cref="Writer"/> instance with the specified parameters.
/// </summary>
public Writer()
{
this.array = ArrayPool<T?>.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8);
this.index = 0;
}
/// <inheritdoc cref="ImmutableArrayBuilder{T}.Count"/>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.index;
}
/// <inheritdoc cref="ImmutableArrayBuilder{T}.WrittenSpan"/>
public ReadOnlySpan<T> WrittenSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(this.array!, 0, this.index);
}
/// <inheritdoc/>
bool ICollection<T>.IsReadOnly => true;
/// <inheritdoc cref="ImmutableArrayBuilder{T}.Add"/>
public void Add(T value)
{
EnsureCapacity(1);
this.array![this.index++] = value;
}
/// <inheritdoc cref="ImmutableArrayBuilder{T}.AddRange"/>
public void AddRange(ReadOnlySpan<T> items)
{
EnsureCapacity(items.Length);
items.CopyTo(this.array.AsSpan(this.index)!);
this.index += items.Length;
}
/// <inheritdoc/>
public void Dispose()
{
T?[]? array = this.array;
this.array = null;
if (array is not null)
{
ArrayPool<T?>.Shared.Return(array, clearArray: typeof(T) != typeof(char));
}
}
/// <inheritdoc/>
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
/// <inheritdoc/>
bool ICollection<T>.Contains(T item)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
Array.Copy(this.array!, 0, array, arrayIndex, this.index);
}
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
T?[] array = this.array!;
int length = this.index;
for (int i = 0; i < length; i++)
{
yield return array[i]!;
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
/// <inheritdoc/>
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
/// <summary>
/// Ensures that <see cref="array"/> has enough free space to contain a given number of new items.
/// </summary>
/// <param name="requestedSize">The minimum number of items to ensure space for in <see cref="array"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int requestedSize)
{
if (requestedSize > this.array!.Length - this.index)
{
ResizeBuffer(requestedSize);
}
}
/// <summary>
/// Resizes <see cref="array"/> to ensure it can fit the specified number of new items.
/// </summary>
/// <param name="sizeHint">The minimum number of items to ensure space for in <see cref="array"/>.</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void ResizeBuffer(int sizeHint)
{
int minimumSize = this.index + sizeHint;
T?[] oldArray = this.array!;
T?[] newArray = ArrayPool<T?>.Shared.Rent(minimumSize);
Array.Copy(oldArray, newArray, this.index);
this.array = newArray;
ArrayPool<T?>.Shared.Return(oldArray, clearArray: typeof(T) != typeof(char));
}
}
}

Просмотреть файл

@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
// more info in ThirdPartyNotices.txt in the root of the project.
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <inheritdoc/>
partial record HierarchyInfo
{
/// <summary>
/// Creates a <see cref="CompilationUnitSyntax"/> instance wrapping the given members.
/// </summary>
/// <param name="memberDeclarations">The input <see cref="MemberDeclarationSyntax"/> instances to use.</param>
/// <returns>A <see cref="CompilationUnitSyntax"/> object wrapping <paramref name="memberDeclarations"/>.</returns>
public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
{
SyntaxToken[] typeModifierTokens = Modifiers.AsImmutableArray().Select(static m => Token((SyntaxKind)m)).ToArray();
// Create the partial type declaration with the given member declarations.
// This code produces a class declaration as follows:
//
// <MODIFIERS> <TYPE_KIND> TYPE_NAME>
// {
// <MEMBERS>
// }
TypeDeclarationSyntax typeDeclarationSyntax =
Hierarchy[0].GetSyntax()
.AddModifiers(typeModifierTokens)
.AddMembers(memberDeclarations.ToArray())
.WithLeadingTrivia(Comment("/// <inheritdoc/>"));
// Add all parent types in ascending order, if any
foreach (TypeInfo parentType in Hierarchy.AsSpan().Slice(1))
{
typeDeclarationSyntax =
parentType.GetSyntax()
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddMembers(typeDeclarationSyntax)
.WithLeadingTrivia(Comment("/// <inheritdoc/>"));
}
// Prepare the leading trivia for the generated compilation unit.
// This will produce code as follows:
//
// <auto-generated/>
// #pragma warning disable
// #nullable enable
SyntaxTriviaList syntaxTriviaList = TriviaList(
Comment("// <auto-generated/>"),
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)),
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)),
Comment(" "));
if (Namespace is "")
{
// If there is no namespace, attach the pragma directly to the declared type,
// and skip the namespace declaration. This will produce code as follows:
//
// <SYNTAX_TRIVIA>
// <TYPE_HIERARCHY>
return
CompilationUnit()
.AddMembers(typeDeclarationSyntax.WithLeadingTrivia(syntaxTriviaList.Add(Comment(" "))))
.NormalizeWhitespace();
}
// Create the compilation unit with disabled warnings, target namespace and generated type.
// This will produce code as follows:
//
// <SYNTAX_TRIVIA>
// namespace <NAMESPACE>
// {
// <TYPE_HIERARCHY>
// }
return
CompilationUnit().AddMembers(
NamespaceDeclaration(IdentifierName(Namespace))
.WithLeadingTrivia(syntaxTriviaList)
.AddMembers(typeDeclarationSyntax))
.NormalizeWhitespace();
}
}

Просмотреть файл

@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
// more info in ThirdPartyNotices.txt in the root of the project.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// A model describing the hierarchy info for a specific type.
/// </summary>
/// <param name="FilenameHint">The filename hint for the current type.</param>
/// <param name="MetadataName">The metadata name for the current type.</param>
/// <param name="Modifiers">The modifiers for the type declaration.</param>
/// <param name="Namespace">Gets the namespace for the current type.</param>
/// <param name="Hierarchy">Gets the sequence of type definitions containing the current type.</param>
internal sealed partial record HierarchyInfo(
string FilenameHint,
string MetadataName,
EquatableArray<ushort> Modifiers,
string Namespace,
EquatableArray<TypeInfo> Hierarchy)
{
/// <summary>
/// Creates a new <see cref="HierarchyInfo"/> instance from a given <see cref="INamedTypeSymbol"/>.
/// </summary>
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to gather info for.</param>
/// <returns>A <see cref="HierarchyInfo"/> instance describing <paramref name="typeSymbol"/>.</returns>
public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
{
using ImmutableArrayBuilder<TypeInfo> hierarchy = ImmutableArrayBuilder<TypeInfo>.Rent();
for (INamedTypeSymbol? parent = typeSymbol;
parent is not null;
parent = parent.ContainingType)
{
hierarchy.Add(new TypeInfo(
parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
parent.TypeKind,
parent.IsRecord));
}
return new(
typeSymbol.GetFullyQualifiedMetadataName(),
typeSymbol.MetadataName,
ImmutableArray.Create((ushort)SyntaxKind.PartialKeyword),
typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
hierarchy.ToImmutable());
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// A model for a singleton service registration.
/// </summary>
/// <param name="RegistrationKind">The registration kind for the service.</param>
/// <param name="ImplementationFullyQualifiedTypeName">The fully qualified type name of the implementation type.</param>
/// <param name="RequiredServiceFullyQualifiedTypeNames">The fully qualified type names of dependent services for <paramref name="ImplementationFullyQualifiedTypeName"/>.</param>
/// <param name="ServiceFullyQualifiedTypeNames">The fully qualified type names for the services to register for <paramref name="ImplementationFullyQualifiedTypeName"/>.</param>
internal sealed record RegisteredServiceInfo(
ServiceRegistrationKind RegistrationKind,
string ImplementationFullyQualifiedTypeName,
EquatableArray<string> RequiredServiceFullyQualifiedTypeNames,
EquatableArray<string> ServiceFullyQualifiedTypeNames);

Просмотреть файл

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// A model for a service collection method.
/// </summary>
/// <param name="Method">The <see cref="ServiceProviderMethodInfo"/> instance for the method.</param>
/// <param name="Services">The sequence of <see cref="RegisteredServiceInfo"/> instances for services to register.</param>
internal sealed record ServiceCollectionInfo(ServiceProviderMethodInfo Method, EquatableArray<RegisteredServiceInfo> Services);

Просмотреть файл

@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// A model for a method producing a service provider.
/// </summary>
/// <param name="Hierarchy">The <see cref="HierarchyInfo"/> instance for the containing type for the method.</param>
/// <param name="MethodName">The method name.</param>
/// <param name="ServiceCollectionParameterName">The name of the service collection parameter.</param>
/// <param name="ReturnsVoid">Whether the method returns <see cref="void"/>.</param>
/// <param name="Modifiers">The method modifiers.</param>
internal sealed record ServiceProviderMethodInfo(
HierarchyInfo Hierarchy,
string MethodName,
string ServiceCollectionParameterName,
bool ReturnsVoid,
EquatableArray<ushort> Modifiers);

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// Indicates the kind of service registration being used.
/// </summary>
internal enum ServiceRegistrationKind
{
/// <summary>
/// A singleton service.
/// </summary>
Singleton,
/// <summary>
/// A transient service.
/// </summary>
Transient
}

Просмотреть файл

@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
/// <summary>
/// A model describing a type info in a type hierarchy.
/// </summary>
/// <param name="QualifiedName">The qualified name for the type.</param>
/// <param name="Kind">The type of the type in the hierarchy.</param>
/// <param name="IsRecord">Whether the type is a record type.</param>
internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord)
{
/// <summary>
/// Creates a <see cref="TypeDeclarationSyntax"/> instance for the current info.
/// </summary>
/// <returns>A <see cref="TypeDeclarationSyntax"/> instance for the current info.</returns>
public TypeDeclarationSyntax GetSyntax()
{
// Create the partial type declaration with the kind.
// This code produces a class declaration as follows:
//
// <TYPE_KIND> <TYPE_NAME>
// {
// }
//
// Note that specifically for record declarations, we also need to explicitly add the open
// and close brace tokens, otherwise member declarations will not be formatted correctly.
return Kind switch
{
TypeKind.Struct => StructDeclaration(QualifiedName),
TypeKind.Interface => InterfaceDeclaration(QualifiedName),
TypeKind.Class when IsRecord =>
RecordDeclaration(Token(SyntaxKind.RecordKeyword), QualifiedName)
.WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken))
.WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)),
_ => ClassDeclaration(QualifiedName)
};
}
}

Просмотреть файл

@ -0,0 +1,343 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Helpers;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators;
/// <inheritdoc/>
partial class ServiceProviderGenerator : IIncrementalGenerator
{
/// <summary>
/// Helpers to generate the service registrations.
/// </summary>
private static class Execute
{
/// <summary>
/// A shared annotation used to track arguments.
/// </summary>
public static readonly SyntaxAnnotation ArgumentAnnotation = new();
/// <summary>
/// Checks whether the input <see cref="SyntaxNode"/> is a valid target for generation.
/// </summary>
/// <param name="syntaxNode">The input <see cref="SyntaxNode"/> instance to analyze.</param>
/// <param name="token">The cancellation token to use.</param>
/// <returns>Whether <paramref name="syntaxNode"/> is a valid generation target.</returns>
public static bool IsSyntaxTarget(SyntaxNode syntaxNode, CancellationToken token)
{
return syntaxNode.IsKind(SyntaxKind.MethodDeclaration);
}
/// <summary>
/// Gathers the info on all registered singleton services.
/// </summary>
/// <param name="context">The current <see cref="GeneratorAttributeSyntaxContext"/> instance with the provided info.</param>
/// <param name="token">The cancellation token to use.</param>
/// <returns>The gathered info for the current service collection, if available.</returns>
public static ServiceCollectionInfo? GetSingletonInfo(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
return GetInfo(ServiceRegistrationKind.Singleton, context, token);
}
/// <summary>
/// Gathers the info on all registered transient services.
/// </summary>
/// <param name="context">The current <see cref="GeneratorAttributeSyntaxContext"/> instance with the provided info.</param>
/// <param name="token">The cancellation token to use.</param>
/// <returns>The gathered info for the current service collection, if available.</returns>
public static ServiceCollectionInfo? GetTransientInfo(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
return GetInfo(ServiceRegistrationKind.Transient, context, token);
}
/// <summary>
/// Gathers the info on all registered services.
/// </summary>
/// <param name="registrationKind">The registration kind to use.</param>
/// <param name="context">The current <see cref="GeneratorAttributeSyntaxContext"/> instance with the provided info.</param>
/// <param name="token">The cancellation token to use.</param>
/// <returns>The gathered info for the current service collection, if available.</returns>
private static ServiceCollectionInfo? GetInfo(ServiceRegistrationKind registrationKind, GeneratorAttributeSyntaxContext context, CancellationToken token)
{
// Ensure that the target syntax node is valid:
// - It has to be a method declaration
// - The method has a single parameter of type Microsoft.Extensions.DependencyInjection.IServiceCollection
// - The method returns void or Microsoft.Extensions.DependencyInjection.IServiceCollection
if (context.TargetNode is not MethodDeclarationSyntax methodDeclaration ||
context.TargetSymbol is not IMethodSymbol { Parameters: [{ } parameterSymbol] } methodSymbol ||
!parameterSymbol.Type.HasFullyQualifiedMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection") ||
!(methodSymbol.ReturnsVoid || methodSymbol.ReturnType.HasFullyQualifiedMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection")))
{
return null;
}
// Gather the basic method info
HierarchyInfo hierarchy = HierarchyInfo.From(methodSymbol.ContainingType);
string methodName = methodSymbol.Name;
token.ThrowIfCancellationRequested();
using ImmutableArrayBuilder<ushort> methodModifiers = ImmutableArrayBuilder<ushort>.Rent();
// Gather all method modifiers
foreach (SyntaxToken modifier in methodDeclaration.Modifiers)
{
methodModifiers.Add((ushort)modifier.Kind());
}
token.ThrowIfCancellationRequested();
using ImmutableArrayBuilder<RegisteredServiceInfo> serviceInfo = ImmutableArrayBuilder<RegisteredServiceInfo>.Rent();
// Gather all registered services
foreach (AttributeData attributeData in context.Attributes)
{
token.ThrowIfCancellationRequested();
if (attributeData.ConstructorArguments is [
{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol { InstanceConstructors: [IMethodSymbol implementationConstructor, ..] } implementationType },
{ Kind: TypedConstantKind.Array, Values: ImmutableArray<TypedConstant> serviceTypes }])
{
// Gather all dependent services for the implementation type
ImmutableArray<string> constructorArgumentTypes = ImmutableArray.CreateRange(
items: implementationConstructor.Parameters,
selector: static parameter => parameter.Type.GetFullyQualifiedName());
string implementationTypeName = implementationType.GetFullyQualifiedName();
ImmutableArray<string> serviceTypeNames = ImmutableArray<string>.Empty;
// If there are no specified service types, use the implementation type itself as service type. This is pretty
// common for eg. factory types, which don't need to be mocked and are just registered as concrete types.
if (serviceTypes.IsEmpty)
{
serviceTypeNames = ImmutableArray.Create(implementationTypeName);
}
else
{
using ImmutableArrayBuilder<string> builder = ImmutableArrayBuilder<string>.Rent();
// Otherwise, simply gather all service types for the current service registration
foreach (TypedConstant serviceType in serviceTypes)
{
if (serviceType is { Kind: TypedConstantKind.Type, Value: INamedTypeSymbol serviceTypeSymbol })
{
builder.Add(serviceTypeSymbol.GetFullyQualifiedName());
}
}
serviceTypeNames = builder.ToImmutable();
}
// Create the model fully describing the current service registration
serviceInfo.Add(new RegisteredServiceInfo(
RegistrationKind: registrationKind,
ImplementationFullyQualifiedTypeName: implementationTypeName,
ServiceFullyQualifiedTypeNames: serviceTypeNames,
RequiredServiceFullyQualifiedTypeNames: constructorArgumentTypes));
}
}
ServiceProviderMethodInfo methodInfo = new(
hierarchy,
methodName,
parameterSymbol.Name,
methodSymbol.ReturnsVoid,
methodModifiers.ToImmutable());
return new(methodInfo, serviceInfo.ToImmutable());
}
/// <summary>
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the gathered info.
/// </summary>
/// <param name="info">The input <see cref="ServiceCollectionInfo"/> instance with the services info.</param>
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the gathered info.</returns>
public static CompilationUnitSyntax GetSyntax(ServiceCollectionInfo info)
{
using ImmutableArrayBuilder<StatementSyntax> registrationStatements = ImmutableArrayBuilder<StatementSyntax>.Rent();
foreach (RegisteredServiceInfo serviceInfo in info.Services)
{
// The first service type always acts as "main" registration, and should always be present
if (serviceInfo.ServiceFullyQualifiedTypeNames.AsSpan() is not [string rootServiceTypeName, ..ReadOnlySpan<string> dependentServiceTypeNames])
{
continue;
}
using ImmutableArrayBuilder<ArgumentSyntax> constructorArguments = ImmutableArrayBuilder<ArgumentSyntax>.Rent();
// Prepare the dependent services for the implementation type
foreach (string constructorServiceType in serviceInfo.RequiredServiceFullyQualifiedTypeNames)
{
// Create an argument for each constructor parameter:
//
// global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredServices<SERVICE_TYPE>(<PARAMETER_NAME>);
constructorArguments.Add(
Argument(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions"),
GenericName(Identifier("GetRequiredService"))
.AddTypeArgumentListArguments(IdentifierName(constructorServiceType))))
.AddArgumentListArguments(Argument(IdentifierName(info.Method.ServiceCollectionParameterName))))
.WithAdditionalAnnotations(ArgumentAnnotation));
}
// Prepare the method name, either AddSingleton or AddTransient
string registrationMethod = $"Add{serviceInfo.RegistrationKind}";
// Special case when the service is a singleton and no dependent services are present, just use eager instantiation instead:
//
// global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(<PARAMETER_NAME>, typeof(<ROOT_SERVICE_TYPE>), new <IMPLEMENTATION_TYPE>());
if (serviceInfo.RegistrationKind == ServiceRegistrationKind.Singleton && constructorArguments.Count == 0)
{
registrationStatements.Add(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions"),
IdentifierName("AddSingleton")))
.AddArgumentListArguments(
Argument(IdentifierName(info.Method.ServiceCollectionParameterName)),
Argument(TypeOfExpression(IdentifierName(rootServiceTypeName))),
Argument(
ObjectCreationExpression(IdentifierName(serviceInfo.ImplementationFullyQualifiedTypeName))
.WithArgumentList(ArgumentList())))));
}
else
{
// Register the main implementation type when at least a dependent service is needed:
//
// global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.<REGISTRATION_METHOD>(<PARAMETER_NAME>, typeof(<ROOT_SERVICE_TYPE>), static services => new <IMPLEMENTATION_TYPE>(<CONSTRUCTOR_ARGUMENTS>));
registrationStatements.Add(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions"),
IdentifierName(registrationMethod)))
.AddArgumentListArguments(
Argument(IdentifierName(info.Method.ServiceCollectionParameterName)),
Argument(TypeOfExpression(IdentifierName(rootServiceTypeName))),
Argument(
SimpleLambdaExpression(Parameter(Identifier("services")))
.AddModifiers(Token(SyntaxKind.StaticKeyword))
.WithExpressionBody(
ObjectCreationExpression(IdentifierName(serviceInfo.ImplementationFullyQualifiedTypeName))
.AddArgumentListArguments(constructorArguments.ToArray()))))));
}
// Register all secondary services, if any
foreach (string dependentServiceType in dependentServiceTypeNames)
{
// Register the main implementation type:
//
// global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.<REGISTRATION_METHOD>(<PARAMETER_NAME>, typeof(<DEPENDENT_SERVICE_TYPE>), static services => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredServices<ROOT_SERVICE_TYPE>(services));
registrationStatements.Add(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions"),
IdentifierName(registrationMethod)))
.AddArgumentListArguments(
Argument(IdentifierName(info.Method.ServiceCollectionParameterName)),
Argument(TypeOfExpression(IdentifierName(dependentServiceType))),
Argument(
SimpleLambdaExpression(Parameter(Identifier("services")))
.AddModifiers(Token(SyntaxKind.StaticKeyword))
.WithExpressionBody(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions"),
GenericName(Identifier("GetRequiredService"))
.AddTypeArgumentListArguments(IdentifierName(rootServiceTypeName))))
.AddArgumentListArguments(Argument(IdentifierName("services"))))))));
}
}
// Return the input service provider, if needed:
//
// return <PARAMETER_NAME>;
if (!info.Method.ReturnsVoid)
{
registrationStatements.Add(ReturnStatement(IdentifierName(info.Method.ServiceCollectionParameterName)).WithLeadingTrivia(Comment(" ")));
}
// Prepare the return type: either void or IServiceCollection
TypeSyntax returnType = info.Method.ReturnsVoid switch
{
true => PredefinedType(Token(SyntaxKind.VoidKeyword)),
false => IdentifierName("global::Microsoft.Extensions.DependencyInjection.IServiceCollection")
};
// Get the service collection configuration method declaration:
//
// /// <inheritdoc/>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// [global::System.Diagnostics.DebuggerNonUserCode]
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
// <MODIFIERS> <RETURN_TYPE> <METHOD_NAME>(global::Microsoft.Extensions.DependencyInjection.IServiceCollection <PARAMETER_NAME>)
// {
// <REGISTRATION_STATEMENTS>
// }
MethodDeclarationSyntax configureServicesMethodDeclaration =
MethodDeclaration(returnType, Identifier(info.Method.MethodName))
.AddModifiers(info.Method.Modifiers.AsImmutableArray().Select(static m => Token((SyntaxKind)m)).ToArray())
.AddParameterListParameters(
Parameter(Identifier(info.Method.ServiceCollectionParameterName))
.WithType(IdentifierName("global::Microsoft.Extensions.DependencyInjection.IServiceCollection")))
.AddBodyStatements(registrationStatements.ToArray())
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ServiceProviderGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ServiceProviderGenerator).Assembly.GetName().Version.ToString())))))),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
.WithLeadingTrivia(Comment("/// <inheritdoc/>"));
// Create the compilation unit with the generated members:
CompilationUnitSyntax compilationUnit = info.Method.Hierarchy.GetCompilationUnit(ImmutableArray.Create<MemberDeclarationSyntax>(configureServicesMethodDeclaration));
// Format the annotations
FormatArgumentNodes(ref compilationUnit, info.Method.Hierarchy.Hierarchy.AsSpan().Length + 3);
return compilationUnit;
}
/// <summary>
/// Formats the arguments with a given annotation adding leading whitespace.
/// </summary>
/// <param name="compilationUnit">The target <see cref="CompilationUnitSyntax"/> instance to modify.</param>
/// <param name="indentationLevel">The indentation level to format arguments for.</param>
private static void FormatArgumentNodes(ref CompilationUnitSyntax compilationUnit, int indentationLevel)
{
string whitespace = new(' ', indentationLevel * 4);
while (compilationUnit.GetAnnotatedNodes(ArgumentAnnotation).FirstOrDefault() is SyntaxNode annotatedNode)
{
// For each argument node, remove the annotation and add a CRLF and the leading whitespace. We can't
// loop over the annotated nodes on the target unit directly, as it's changed for every iteration.
compilationUnit = compilationUnit.ReplaceNode(
annotatedNode,
annotatedNode.WithoutAnnotations(ArgumentAnnotation).WithLeadingTrivia(CarriageReturnLineFeed, Whitespace(whitespace)));
}
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Extensions;
using CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.Models;
namespace CommunityToolkit.Extensions.DependencyInjection.SourceGenerators;
/// <summary>
/// A source generator for <see cref="System.IServiceProvider"/> instances.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed partial class ServiceProviderGenerator : IIncrementalGenerator
{
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Gather info on all singleton service providers to generate
IncrementalValuesProvider<ServiceCollectionInfo> singletonServices =
context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CommunityToolkit.Extensions.DependencyInjection.SingletonAttribute",
predicate: Execute.IsSyntaxTarget,
transform: Execute.GetSingletonInfo)
.Where(static info => info is not null)!;
// Do the same for all transient services
IncrementalValuesProvider<ServiceCollectionInfo> transientServices =
context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CommunityToolkit.Extensions.DependencyInjection.TransientAttribute",
predicate: Execute.IsSyntaxTarget,
transform: Execute.GetTransientInfo)
.Where(static info => info is not null)!;
// Merge the two registration types
IncrementalValuesProvider<ServiceCollectionInfo> serviceCollections = singletonServices.Concat(transientServices);
// Aggregate all discovered services (both singleton and transient) and group them by target method
IncrementalValuesProvider<ServiceCollectionInfo> groupedServiceCollections =
serviceCollections.GroupBy<ServiceCollectionInfo, ServiceProviderMethodInfo, RegisteredServiceInfo>(
keySelector: static item => item.Method,
elementsSelector: static item => item.Services,
resultSelector: static (key, elements) => new ServiceCollectionInfo(key, elements));
// Generate all service provider methods
context.RegisterSourceOutput(groupedServiceCollections, static (context, info) =>
{
CompilationUnitSyntax compilationUnit = Execute.GetSyntax(info);
context.AddSource($"{info.Method.Hierarchy.FilenameHint}.{info.Method.MethodName}.g.cs", compilationUnit.GetText(Encoding.UTF8));
});
}
}

Просмотреть файл

@ -0,0 +1,3 @@
@ECHO OFF
powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*

Просмотреть файл

@ -0,0 +1,38 @@
<Project Sdk="MSBuild.Sdk.Extras/3.0.23">
<PropertyGroup>
<ToolkitComponentName>Extensions.DependencyInjection</ToolkitComponentName>
<Description>This package contains Extensions.DependencyInjection.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.0.1</Version>
<RootNamespace>CommunityToolkit.Extensions.DependencyInjection</RootNamespace>
<PackageId>$(PackageIdPrefix).$(ToolkitComponentName)</PackageId>
</PropertyGroup>
<!-- Sets this up as a toolkit component's source project -->
<Import Project="$(ToolingDirectory)\ToolkitComponent.SourceProject.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
</ItemGroup>
<!-- Source generator project reference for packing -->
<ItemGroup>
<ProjectReference Include="..\CommunityToolkit.Extensions.DependencyInjection.SourceGenerators\CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<!-- Pack the source generator and the .targets file -->
<ItemGroup Label="Package">
<!-- Include the custom .targets file to check the source generator -->
<None Include="CommunityToolkit.Extensions.DependencyInjection.targets" PackagePath="buildTransitive\netstandard2.0" Pack="true" />
<None Include="CommunityToolkit.Extensions.DependencyInjection.targets" PackagePath="build\netstandard2.0" Pack="true" />
<!-- Pack the source generator to the right package folder -->
<None Include="..\CommunityToolkit.Extensions.DependencyInjection.SourceGenerators\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.dll" PackagePath="analyzers\dotnet\cs" Pack="true" Visible="false" />
</ItemGroup>
<!-- Remove imported global usings -->
<ItemGroup>
<Compile Remove="$(ToolingDirectory)\GlobalUsings.cs" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,104 @@
<Project>
<!-- Get the analyzer from the CommunityToolkit.Labs.Extensions.DependnecyInjection NuGet package -->
<Target Name="CommunityToolkitExtensionsDependencyInjectionGatherAnalyzers">
<ItemGroup>
<CommunityToolkitExtensionsDependencyInjectionAnalyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == 'CommunityToolkit.Labs.Extensions.DependnecyInjection'" />
</ItemGroup>
</Target>
<!-- Remove the analyzer if Roslyn is missing -->
<Target Name="CommunityToolkitExtensionsDependencyInjectionRemoveAnalyzersForRosynNotFound"
Condition="'$(CSharpCoreTargetsPath)' == ''"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
DependsOnTargets="CommunityToolkitExtensionsDependencyInjectionGatherAnalyzers">
<!-- If no Roslyn assembly could be found, just remove the analyzer without emitting a warning -->
<ItemGroup>
<Analyzer Remove="@(CommunityToolkitExtensionsDependencyInjectionAnalyzer)"/>
</ItemGroup>
</Target>
<!-- Remove the analyzer if using Roslyn 3.x (incremental generators require Roslyn 4.x) -->
<Target Name="CommunityToolkitExtensionsDependencyInjectionRemoveAnalyzersForRoslyn3"
Condition="'$(CSharpCoreTargetsPath)' != ''"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
DependsOnTargets="CommunityToolkitExtensionsDependencyInjectionGatherAnalyzers">
<!--
Use the CSharpCoreTargetsPath property to find the version of the compiler we are using. This is the same mechanism
MSBuild uses to find the compiler. We could check the assembly version for any compiler assembly (since they all have
the same version) but Microsoft.Build.Tasks.CodeAnalysis.dll is where MSBuild loads the compiler tasks from so if
someone is getting creative with msbuild tasks/targets this is the "most correct" assembly to check.
-->
<GetAssemblyIdentity AssemblyFiles="$([System.IO.Path]::Combine(`$([System.IO.Path]::GetDirectoryName($(CSharpCoreTargetsPath)))`,`Microsoft.Build.Tasks.CodeAnalysis.dll`))">
<Output TaskParameter="Assemblies" ItemName="CommunityToolkitExtensionsDependencyInjectionCurrentCompilerAssemblyIdentity"/>
</GetAssemblyIdentity>
<PropertyGroup>
<!-- Transform the resulting item from GetAssemblyIdentity into a property representing its assembly version -->
<CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersion>@(CommunityToolkitExtensionsDependencyInjectionCurrentCompilerAssemblyIdentity->'%(Version)')</CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersion>
<!-- The CurrentCompilerVersionIsNotNewEnough property can now be defined based on the Roslyn assembly version -->
<CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersionIsNotNewEnough Condition="$([MSBuild]::VersionLessThan($(CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersion), 4.3))">true</CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersionIsNotNewEnough>
</PropertyGroup>
<!-- If the Roslyn version is < 4.3, disable the source generators -->
<ItemGroup Condition ="'$(CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersionIsNotNewEnough)' == 'true'">
<Analyzer Remove="@(CommunityToolkitExtensionsDependencyInjectionAnalyzer)"/>
</ItemGroup>
<!--
If the source generators are disabled, also emit a warning. This would've been produced by MSBuild itself as well, but
emitting this manually lets us customize the message to inform developers as to why exactly the generators have been
disabled, and that the rest of the MVVM Toolkit will still keep working as intended, just without additional features.
-->
<Warning Condition ="'$(CommunityToolkitExtensionsDependencyInjectionCurrentCompilerVersionIsNotNewEnough)' == 'true'"
Code="TKEXDICFG0001"
Text="The CommunityToolkit.Extensions.DependencyInjection source generators have been disabled on the current configuration, as they need Roslyn 4.3 in order to work. The CommunityToolkit.Extensions.DependencyInjection APIs will work just fine, but features relying on the source generators will not be available."/>
</Target>
<!--
Inform the user if packages.config is used (as the analyzers and the source generators
won't work at all). Since packages.config can only be used with legacy-style projects,
the entire package can be skipped if an SDK-style project is used.
-->
<Target Name="CommunityToolkitExtensionsDependencyInjectionWarnForPackagesConfigUse"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
Condition="'$(UsingMicrosoftNetSDK)' != 'true'">
<!--
Check whether packages are being restored via packages.config, by reading the associated MSBuild property.
This happens when either the project style is using packages.config, or when explicitly requested.
See https://learn.microsoft.com/nuget/reference/msbuild-targets#restoring-packagereference-and-packagesconfig-projects-with-msbuild.
-->
<PropertyGroup>
<CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig Condition ="'$(RestorePackagesConfig)' == 'true' OR '$(RestoreProjectStyle)' == 'PackagesConfig'">true</CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig>
</PropertyGroup>
<!--
If no packages.config properties are set, also try to manually find the packages.config file.
This will be in the @(None) elements, if present. Doing so makes sure this works in builds as
well, since the implicit targets populating the properties above only run when restoring.
Since the packages.config file will always be in the root of the project, if present, we will
match with the full item spec (see https://learn.microsoft.com/nuget/reference/packages-config).
-->
<FindInList ItemSpecToFind="packages.config"
List="@(None)"
MatchFileNameOnly="false"
Condition="'$(CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig)' != 'true'">
<Output TaskParameter="ItemFound" PropertyName="CommunityToolkitExtensionsDependencyInjectionPackagesConfigFile"/>
</FindInList>
<!-- Make sure to update the MSBuild property if the above task did find something -->
<PropertyGroup>
<CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig Condition ="'$(CommunityToolkitExtensionsDependencyInjectionPackagesConfigFile)' == 'packages.config'">true</CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig>
</PropertyGroup>
<!-- Emit a warning in case packages.config is used -->
<Warning Condition ="'$(CommunityToolkitExtensionsDependencyInjectionIsTargetProjectUsingPackagesConfig)' == 'true'"
Code="TKEXDICFG0002"
Text="The CommunityToolkit.Extensions.DependencyInjection source generators might not be loaded correctly, as the current project is using the packages.config setup to restore NuGet packages. Source generators require PackageReference to be used (either in a legacy-style or SDK-style .csproj project, both are supported as long as PackageReference is used)."/>
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<!--
MultiTarget is a custom property that indicates which target a project is designed to be built for / run on.
Used to create project references, generate solution files, enable/disable TargetFrameworks, and build nuget packages.
-->
<MultiTarget>netstandard;</MultiTarget>
</PropertyGroup>
</Project>

Просмотреть файл

@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
namespace CommunityToolkit.Extensions.DependencyInjection;
/// <summary>
/// <para>
/// An attribute that can be used to instruct the generator to add a singleton service to the target <see cref="IServiceCollection"/> instance.
/// </para>
/// <para>
/// This attribute should be added to a <see langword="partial"/> method receiving an <see cref="IServiceCollection"/>
/// instance, and the generator will register all requested services (optionally also returning the input object).
/// </para>
/// <para>
/// That is, given a declaration as follows:
/// <code>
/// [Singleton(typeof(MyServiceA), typeof(IMyServiceA))]
/// [Singleton(typeof(MyServiceB), typeof(IMyServiceB))]
/// [Singleton(typeof(MyServiceC), typeof(IMyServiceC))]
/// private static partial void ConfigureServices(IServiceCollection services);
/// </code>
/// The generator will produce code as follows:
/// <code>
/// private static partial void ConfigureServices(IServiceCollection services)
/// {
/// services.AddSingleton(typeof(IMyServiceA), static services => new MyServiceA());
/// services.AddSingleton(typeof(IMyServiceB), static services => new MyServiceB(
/// services.GetRequiredServices&lt;IMyServiceA&gt;()));
/// services.AddSingleton(typeof(IMyServiceC), static services => new MyServiceC(
/// services.GetRequiredServices&lt;IMyServiceA&gt;(),
/// services.GetRequiredServices&lt;IMyServiceB&gt;()));
/// }
/// </code>
/// </para>
/// </summary>
/// <remarks>
/// This attribute is conditional for two reasons:
/// <list type="bullet">
/// <item>
/// Since the attributes are only used for source generation and there can be a large number of them, this
/// reduces the metadata impact on the final assemblies. If needed, the directive can be manually defined.
/// </item>
/// <item>
/// The attributes have a constructor parameter of an array type, which is not allowed in WinRT assemblies.
/// Making the attributes conditional makes Roslyn skip emitting them, which avoids WinMDExp generating an
/// invalid PE file and then causing projects referencing it to fail to build. For more info on the WinMDExp
/// issue, see <see href="https://developercommunity.visualstudio.com/t/MSBuild:-OutOfMemoryException:-Task-Gen/10270567?"/>.
/// </item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
[Conditional("SERVICES_CONFIGURATION_METADATA")]
public sealed class SingletonAttribute : Attribute
{
/// <summary>
/// Creates a new <see cref="SingletonAttribute"/> instance with the specified parameters.
/// </summary>
/// <param name="implementationType">The implementation type for the service.</param>
/// <param name="serviceTypes">The service types to register for the provided implementation.</param>
public SingletonAttribute(Type implementationType, params Type[] serviceTypes)
{
ImplementationType = implementationType;
ServiceTypes = serviceTypes;
}
/// <summary>
/// Gets the implementation type for the service to register.
/// </summary>
public Type ImplementationType { get; }
/// <summary>
/// Gets the supported service types for the implementation being registered.
/// </summary>
public Type[] ServiceTypes { get; }
}

Просмотреть файл

@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
namespace CommunityToolkit.Extensions.DependencyInjection;
/// <summary>
/// <para>
/// An attribute that can be used to instruct the generator to add a transient service to the input <see cref="IServiceCollection"/> instance.
/// </para>
/// <para>
/// This attribute can be used in the same way as <see cref="SingletonAttribute"/>, the only difference being that it will register transient services.
/// A method can be annotated with any combination of <see cref="SingletonAttribute"/> and <see cref="TransientAttribute"/>.
/// </para>
/// </summary>
/// <remarks>For more info, see <seealso cref="SingletonAttribute"/>.</remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
[Conditional("SERVICES_CONFIGURATION_METADATA")]
public sealed class TransientAttribute : Attribute
{
/// <summary>
/// Creates a new <see cref="TransientAttribute"/> instance with the specified parameters.
/// </summary>
/// <param name="implementationType">The implementation type for the service.</param>
/// <param name="serviceTypes">The service types to register for the provided implementation.</param>
public TransientAttribute(Type implementationType, params Type[] serviceTypes)
{
ImplementationType = implementationType;
ServiceTypes = serviceTypes;
}
/// <summary>
/// Gets the implementation type for the service to register.
/// </summary>
public Type ImplementationType { get; }
/// <summary>
/// Gets the supported service types for the implementation being registered.
/// </summary>
public Type[] ServiceTypes { get; }
}

Просмотреть файл

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>47BFA618-6EF6-426A-B7CB-D8F2CEA68302</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Extensions.DependencyInjectionExperiment.Tests</Import_RootNamespace>
</PropertyGroup>
</Project>

Просмотреть файл

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>47BFA618-6EF6-426A-B7CB-D8F2CEA68302</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Extensions.DependencyInjection.Tests.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

@ -1 +1 @@
Subproject commit a852f23dabb110b7a51c068662309d00834d90a1
Subproject commit 57bce71cea3e692ae1dac0ba811494345e8954d9