Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
; 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
--------|----------|----------|-------
APPSRVSPR0001 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error |
APPSRVSPR0002 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error |
APPSRVSPR0003 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error |
APPSRVSPR0004 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error |
APPSRVSPR0005 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error |
APPSRVSPR0006 | CommunityToolkit.AppServices.SourceGenerators.InvalidValueSetSerializerUseAnalyzer | Error |
APPSRVSPR0007 | CommunityToolkit.AppServices.SourceGenerators.InvalidValueSetSerializerUseAnalyzer | Warning |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.AppServices.SourceGenerators;

/// <inheritdoc/>
partial class AppServiceGenerator : IIncrementalGenerator
{
/// <summary>
/// Shader generators logic for app service hosts and components.
/// </summary>
private static class Helpers
{
/// <summary>
/// Gets whether the current target is a UWP application.
/// </summary>
/// <param name="compilation">The input <see cref="Compilation"/> instance to inspect.</param>
/// <returns>Whether the current target is a UWP application.</returns>
public static bool IsUwpTarget(Compilation compilation)
{
return compilation.Options.OutputKind == OutputKind.WindowsRuntimeApplication;
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using CommunityToolkit.AppServices.SourceGenerators.Extensions;
using CommunityToolkit.AppServices.SourceGenerators.Models;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace CommunityToolkit.AppServices.SourceGenerators;

/// <summary>
/// A source generator for the <c>AppServiceAttribute</c> type.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed partial class AppServiceGenerator : IIncrementalGenerator
{
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Get all app service class implementations, and only enable this branch if the target is not a UWP app (the component)
IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceComponentInfo =
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax classDeclaration && classDeclaration.HasOrPotentiallyHasBaseTypes(),
static (context, token) =>
{
// Only retrieve host info if the target is not a UWP application
if (Helpers.IsUwpTarget(context.SemanticModel.Compilation))
Comment thread
Sergio0694 marked this conversation as resolved.
{
return default;
}

INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node, token)!;

// Only select the first declaration of a given item, to avoid issues with partial types
if (!context.Node.IsFirstSyntaxDeclarationForSymbol(typeSymbol))
{
return default;
}

token.ThrowIfCancellationRequested();

// Try to get the info on the current component
(INamedTypeSymbol? serviceSymbol, string? appServiceName) = Component.GetInfo(typeSymbol, token);

// If there's no app service interface, do nothing
if (serviceSymbol is null)
{
return default;
}

HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol);

token.ThrowIfCancellationRequested();

ImmutableArray<MethodInfo> methods = MethodInfo.From(serviceSymbol, token);

token.ThrowIfCancellationRequested();

return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName!, typeSymbol.GetFullyQualifiedName()));
})
.Where(static pair => pair.Hierarchy is not null);

// Produce the component type
context.RegisterSourceOutput(appServiceComponentInfo, static (context, item) =>
{
ConstructorDeclarationSyntax constructorSyntax = Component.GetSyntax(item.Hierarchy, item.Info);
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(
ImmutableArray.Create<MemberDeclarationSyntax>(constructorSyntax),
ImmutableArray.Create<BaseTypeSyntax>(SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceComponent"))),
"/// <inheritdoc/>");

context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8));
});

// Gather all interfaces, and only enable this branch if the target is a UWP app (the host)
IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceHostInfo =
context.SyntaxProvider
.ForAttributeWithMetadataName(
"CommunityToolkit.AppServices.AppServiceAttribute",
static (node, _) => node is InterfaceDeclarationSyntax,
static (context, token) =>
{
// Only retrieve host info if the target is a UWP application
if (!Helpers.IsUwpTarget(context.SemanticModel.Compilation))
{
return default;
}

// Check if the current interface is in fact an app service type
if (!Host.TryGetAppServiceName(context.Attributes[0], out string? appServiceName))
{
return default;
}

token.ThrowIfCancellationRequested();

INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.TargetSymbol;

// Get the info on the host implementation
HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol, typeSymbol.Name.Substring(1));

token.ThrowIfCancellationRequested();

ImmutableArray<MethodInfo> methods = MethodInfo.From(typeSymbol, token);

token.ThrowIfCancellationRequested();

return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName, typeSymbol.GetFullyQualifiedName()));
})
.Where(static item => item.Hierarchy is not null);

// Produce the host type
context.RegisterSourceOutput(appServiceHostInfo, static (context, item) =>
{
ConstructorDeclarationSyntax constructorSyntax = Host.GetConstructorSyntax(item.Hierarchy, item.Info);
ImmutableArray<MethodDeclarationSyntax> methodDeclarations = Host.GetMethodDeclarationsSyntax(item.Info);
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(
ImmutableArray.Create<MemberDeclarationSyntax>(constructorSyntax).AddRange(methodDeclarations),
ImmutableArray.Create<BaseTypeSyntax>(
SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceHost")),
SimpleBaseType(IdentifierName(item.Info.InterfaceFullyQualifiedName))),
$"/// <summary>A generated host implementation for the <see cref=\"{item.Info.InterfaceFullyQualifiedName}\"/> interface.</summary>");

context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8));
});
}
}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using CommunityToolkit.AppServices.SourceGenerators.Extensions;
using CommunityToolkit.AppServices.SourceGenerators.Models;
using static CommunityToolkit.AppServices.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.AppServices.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that emits diagnostics whenever an app service has an invalid member.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidAppServicesMemberAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
InvalidAppServicesMemberType,
InvalidAppServicesMethodReturnType,
InvalidAppServicesMethodParameterType,
InvalidRepeatedAppServicesMethodIProgressParameter,
InvalidRepeatedAppServicesMethodCancellationTokenParameter);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// Get the symbol for [AppService]
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.AppServices.AppServiceAttribute") is not INamedTypeSymbol appServicesAttributeSymbol)
{
return;
}

// Register a callback for all named type symbols (ie. user defined types)
context.RegisterSymbolAction(context =>
{
// The symbol must be an interface
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol)
{
return;
}

// Check whether the interface is an app services interface
if (!interfaceSymbol.HasOrInheritsAttribute(appServicesAttributeSymbol))
{
return;
}

// Go through all interface members to analyze them. Here we need to go through all members, not just the ones immediately
// declared, as it's possible an interface with an invalid member will be inherited by another one that adds [AppServices].
// In that case, the base interface will not be analyzed (as it doesn't have [AppServices]), so the derived one will need
// to also go through inherited members to ensure that all members that the generator will process will actually be valid.
foreach (ISymbol memberSymbol in interfaceSymbol.GetAllMembers())
{
// If a method is not abstract nor virtual (ie. a DIM or static non-virtual interface member), it can just be ignored.
// The generated service type will not have to consider it as far as registering endpoints and generating members goes.
if (memberSymbol.IsIgnoredAppServicesMember())
{
continue;
}

// All remaining members must be non-generic instance methods, which the generator will emit
if (memberSymbol is not IMethodSymbol { IsStatic: false, IsGenericMethod: false, ReturnType: INamedTypeSymbol returnTypeSymbol } methodSymbol)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMemberType, memberSymbol.Locations.FirstOrDefault(), memberSymbol, interfaceSymbol));

continue;
}

// Validate the return type for the current method
if (!methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType) ||
!returnType.IsValidReturnType())
{
context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodReturnType, memberSymbol.Locations.FirstOrDefault(), methodSymbol, interfaceSymbol, returnTypeSymbol));
}

bool isProgressParameterFound = false;
bool isCancellationTokenParameterFound = false;

// Validate the method parameters
foreach (IParameterSymbol parameter in methodSymbol.Parameters)
{
// First validate types that could possibly be allowed at all (ie. valid types)
if (!parameter.TryGetParameterOrReturnType(out ParameterOrReturnType parameterType) ||
!parameterType.IsValidParameterType())
{
context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodParameterType, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type));

continue;
}

// Then check that the type is not an IProgress<T>, if one has already been discovered
if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT))
{
if (isProgressParameterFound)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodIProgressParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type));
}

isProgressParameterFound = true;
}

// Lastly, check that the type is not a CancellationToken, if one has already been discovered
if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken))
{
if (isCancellationTokenParameterFound)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type));
}

isCancellationTokenParameterFound = true;
}
}
}
}, SymbolKind.NamedType);
});
}
}
Loading