// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.Build.TaskAuthoring.Analyzer
{
/// <summary>
/// Validates that a task's two multithreading declarations agree.
/// <para>
/// <c>[MSBuildMultiThreadableTask]</c> is the routing signal: it is the only thing
/// <c>TaskRouter.NeedsTaskHostInMultiThreadedMode</c> reads, and without it a task runs in an
/// out-of-proc TaskHost sidecar. <c>IMultiThreadableTask</c> is the injection signal:
/// <c>TaskExecutionHost</c> assigns <c>TaskEnvironment</c> only to instances of that interface.
/// </para>
/// <para>
/// Declaring one without the other is legal but usually unintended, and both halves fail silently.
/// The attribute is also reported when it is applied to a type MSBuild never routes as a task --
/// one that is not a task at all, or an abstract task whose attribute no subclass inherits.
/// </para>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MultiThreadableTaskDeclarationAnalyzer : DiagnosticAnalyzer
{
private const string TaskEnvironmentPropertyName = "TaskEnvironment";
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
DiagnosticDescriptors.TaskEnvironmentNeverAssigned,
DiagnosticDescriptors.MissingMultiThreadableTaskAttribute,
DiagnosticDescriptors.MultiThreadableTaskAttributeHasNoEffect);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(OnCompilationStart);
}
private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
INamedTypeSymbol? taskType = context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.ITaskFullName);
INamedTypeSymbol? multiThreadableTaskType =
context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.IMultiThreadableTaskFullName);
INamedTypeSymbol? taskEnvironmentType =
context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.TaskEnvironmentFullName);
INamedTypeSymbol? attributeType =
context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.MultiThreadableTaskAttributeFullName);
if (taskType is null || multiThreadableTaskType is null || taskEnvironmentType is null || attributeType is null)
{
return;
}
context.RegisterSymbolAction(
symbolContext => AnalyzeNamedType(
symbolContext,
taskType,
multiThreadableTaskType,
taskEnvironmentType,
attributeType),
SymbolKind.NamedType);
}
private static void AnalyzeNamedType(
SymbolAnalysisContext context,
INamedTypeSymbol taskType,
INamedTypeSymbol multiThreadableTaskType,
INamedTypeSymbol taskEnvironmentType,
INamedTypeSymbol attributeType)
{
var type = (INamedTypeSymbol)context.Symbol;
if (type.TypeKind != TypeKind.Class)
{
return;
}
bool hasAttribute = HasMultiThreadableTaskAttribute(type, attributeType);
if (!SharedAnalyzerHelpers.ImplementsInterface(type, taskType))
{
// TaskRouter only inspects types the engine is about to run as a task, so the routing
// attribute does nothing here. This usually means it landed on a helper type, or on the
// wrong class of a multi-class file, leaving the real task unmarked.
if (hasAttribute)
{
ReportNoEffect(context, type, "it does not implement ITask");
}
return;
}
// The engine never instantiates an abstract type, and TaskRouter reads the attribute with
// inherit: false off the concrete type it did instantiate. An attribute here reaches nothing:
// every derived task is still routed to a TaskHost. Neither MSBuildTask0012 nor
// MSBuildTask0013 is meaningful on an abstract type either.
if (type.IsAbstract)
{
if (hasAttribute)
{
ReportNoEffect(
context,
type,
"the engine never instantiates an abstract task and the attribute is not inherited, so derived tasks do not pick it up");
}
return;
}
bool implementsInterface = SharedAnalyzerHelpers.ImplementsInterface(type, multiThreadableTaskType);
if (hasAttribute && !implementsInterface)
{
// The attribute on its own is a supported state: it declares a task safe to run
// in-process without giving it access to TaskEnvironment. Only report when the task
// also declares a TaskEnvironment property, which says the author expects the engine
// to populate it -- and it never will.
if (HasTaskEnvironmentProperty(type, taskEnvironmentType) &&
!HasTaskEnvironmentConstructor(type, taskEnvironmentType))
{
ReportOnType(context, DiagnosticDescriptors.TaskEnvironmentNeverAssigned, type);
}
}
else if (!hasAttribute && SharedAnalyzerHelpers.DeclaresMultiThreadableTaskInterface(type, multiThreadableTaskType))
{
ReportOnType(context, DiagnosticDescriptors.MissingMultiThreadableTaskAttribute, type);
}
}
private static bool HasMultiThreadableTaskAttribute(INamedTypeSymbol type, INamedTypeSymbol attributeType)
{
// Matches TaskRouter, which reads the attribute with inherit: false.
foreach (AttributeData attribute in type.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType))
{
return true;
}
}
return false;
}
private static bool HasTaskEnvironmentProperty(INamedTypeSymbol type, INamedTypeSymbol taskEnvironmentType)
{
foreach (IPropertySymbol property in SharedAnalyzerHelpers.GetPropertiesIncludingBaseTypes(type))
{
if (property.Name == TaskEnvironmentPropertyName &&
SymbolEqualityComparer.Default.Equals(property.Type, taskEnvironmentType) &&
property.SetMethod is not null)
{
return true;
}
}
return false;
}
/// <summary>
/// The engine selects a single-parameter <c>TaskEnvironment</c> constructor by signature alone,
/// independently of <c>IMultiThreadableTask</c>, so such a task does receive an environment.
/// </summary>
private static bool HasTaskEnvironmentConstructor(INamedTypeSymbol type, INamedTypeSymbol taskEnvironmentType)
{
foreach (IMethodSymbol constructor in type.InstanceConstructors)
{
if (constructor.DeclaredAccessibility == Accessibility.Public &&
constructor.Parameters.Length == 1 &&
SymbolEqualityComparer.Default.Equals(constructor.Parameters[0].Type, taskEnvironmentType))
{
return true;
}
}
return false;
}
private static void ReportOnType(SymbolAnalysisContext context, DiagnosticDescriptor descriptor, INamedTypeSymbol type)
{
foreach (Location location in type.Locations)
{
if (location.IsInSource)
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, type.Name));
return;
}
}
}
/// <summary>
/// Reports that the routing attribute cannot take effect on this type, naming the reason so the
/// two shapes -- not a task at all, and an abstract task whose attribute no subclass inherits --
/// are distinguishable in the message.
/// </summary>
private static void ReportNoEffect(SymbolAnalysisContext context, INamedTypeSymbol type, string reason)
{
foreach (Location location in type.Locations)
{
if (location.IsInSource)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MultiThreadableTaskAttributeHasNoEffect,
location,
type.Name,
reason));
return;
}
}
}
}
}