Skip to content
Draft
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
24 changes: 24 additions & 0 deletions src/Analyzers/KnownTypeSymbols.Net.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public sealed partial class KnownTypeSymbols
INamedTypeSymbol? environment;
INamedTypeSymbol? httpClient;
INamedTypeSymbol? iLogger;
INamedTypeSymbol? iConfiguration;
INamedTypeSymbol? iOptions;
INamedTypeSymbol? iOptionsSnapshot;
INamedTypeSymbol? iOptionsMonitor;

/// <summary>
/// Gets a Guid type symbol.
Expand Down Expand Up @@ -81,4 +85,24 @@ public sealed partial class KnownTypeSymbols
/// Gets an ILogger type symbol.
/// </summary>
public INamedTypeSymbol? ILogger => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Logging.ILogger", ref this.iLogger);

/// <summary>
/// Gets an IConfiguration type symbol.
/// </summary>
public INamedTypeSymbol? IConfiguration => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Configuration.IConfiguration", ref this.iConfiguration);

/// <summary>
/// Gets an IOptions type symbol.
/// </summary>
public INamedTypeSymbol? IOptions => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Options.IOptions`1", ref this.iOptions);

/// <summary>
/// Gets an IOptionsSnapshot type symbol.
/// </summary>
public INamedTypeSymbol? IOptionsSnapshot => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Options.IOptionsSnapshot`1", ref this.iOptionsSnapshot);

/// <summary>
/// Gets an IOptionsMonitor type symbol.
/// </summary>
public INamedTypeSymbol? IOptionsMonitor => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Options.IOptionsMonitor`1", ref this.iOptionsMonitor);
}
109 changes: 100 additions & 9 deletions src/Analyzers/Orchestration/EnvironmentOrchestrationAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ public sealed class EnvironmentOrchestrationVisitor : MethodProbeOrchestrationVi
/// <inheritdoc/>
public override bool Initialize()
{
return this.KnownTypeSymbols.Environment != null;
return this.KnownTypeSymbols.Environment != null
|| this.KnownTypeSymbols.IConfiguration != null
|| this.KnownTypeSymbols.IOptions != null
|| this.KnownTypeSymbols.IOptionsSnapshot != null
|| this.KnownTypeSymbols.IOptionsMonitor != null;
}

/// <inheritdoc/>
Expand All @@ -57,19 +61,106 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth
return;
}

foreach (IInvocationOperation invocation in methodOperation.Descendants().OfType<IInvocationOperation>())
foreach (IOperation operation in methodOperation.Descendants())
{
IMethodSymbol targetMethod = invocation.TargetMethod;
this.CheckOperationForEnvironmentVariableAccess(operation, methodSymbol, orchestrationName, reportDiagnostic);
}
}

if (targetMethod.ContainingType.Equals(this.KnownTypeSymbols.Environment, SymbolEqualityComparer.Default) &&
targetMethod.Name is nameof(Environment.GetEnvironmentVariable) or nameof(Environment.GetEnvironmentVariables) or nameof(Environment.ExpandEnvironmentVariables))
{
string invocationName = targetMethod.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
void CheckOperationForEnvironmentVariableAccess(IOperation operation, IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
{
switch (operation)
{
case IInvocationOperation invocation when this.IsEnvironmentInvocation(invocation.TargetMethod):
this.Report(operation, invocation.TargetMethod.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), methodSymbol, orchestrationName, reportDiagnostic);
break;
case IInvocationOperation invocation when this.IsConfigurationOrOptionsInvocation(invocation):
this.Report(operation, invocation.TargetMethod.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), methodSymbol, orchestrationName, reportDiagnostic);
break;
case IPropertyReferenceOperation propertyReference when this.IsConfigurationOrOptionsPropertyReference(propertyReference):
this.Report(operation, propertyReference.Property.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), methodSymbol, orchestrationName, reportDiagnostic);
break;
}
}

void Report(IOperation operation, string memberName, IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
{
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation, methodSymbol.Name, memberName, orchestrationName));
}

bool IsEnvironmentInvocation(IMethodSymbol targetMethod)
{
return this.KnownTypeSymbols.Environment != null &&
targetMethod.ContainingType.Equals(this.KnownTypeSymbols.Environment, SymbolEqualityComparer.Default) &&
targetMethod.Name is nameof(Environment.GetEnvironmentVariable) or nameof(Environment.GetEnvironmentVariables) or nameof(Environment.ExpandEnvironmentVariables);
}

bool IsConfigurationOrOptionsInvocation(IInvocationOperation invocation)
{
if (this.IsConfigurationOrOptionsType(invocation.Instance?.Type))
{
return true;
}

// e.g.: "The method 'Method1' uses environment variables through 'Environment.GetEnvironmentVariable()' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, invocation, methodSymbol.Name, invocationName, orchestrationName));
if (invocation.TargetMethod.IsExtensionMethod)
{
ITypeSymbol? receiverType = invocation.TargetMethod.ReducedFrom?.Parameters.FirstOrDefault()?.Type ?? invocation.TargetMethod.Parameters.FirstOrDefault()?.Type;
if (this.IsConfigurationOrOptionsType(receiverType))
{
return true;
}
}

return false;
}

bool IsConfigurationOrOptionsPropertyReference(IPropertyReferenceOperation propertyReference)
{
if (this.IsConfigurationOrOptionsType(propertyReference.Instance?.Type))
{
return true;
}

return this.IsConfigurationOrOptionsType(propertyReference.Property.ContainingType);
}

bool IsConfigurationOrOptionsType(ITypeSymbol? type)
{
if (type is null)
{
return false;
}

if (this.IsConfigurationType(type))
{
return true;
}

if (type is INamedTypeSymbol namedType && this.IsOptionsType(namedType))
{
return true;
}

return type.AllInterfaces.Any(this.IsConfigurationType) ||
(type is INamedTypeSymbol typeSymbol && typeSymbol.AllInterfaces.Any(this.IsOptionsType));
}

bool IsConfigurationType(ITypeSymbol type)
{
return this.KnownTypeSymbols.IConfiguration != null &&
SymbolEqualityComparer.Default.Equals(type, this.KnownTypeSymbols.IConfiguration);
}

bool IsOptionsType(INamedTypeSymbol type)
{
return this.IsOptionsType(type, this.KnownTypeSymbols.IOptions)
|| this.IsOptionsType(type, this.KnownTypeSymbols.IOptionsSnapshot)
|| this.IsOptionsType(type, this.KnownTypeSymbols.IOptionsMonitor);
}

bool IsOptionsType(INamedTypeSymbol type, INamedTypeSymbol? optionsSymbol)
{
return optionsSymbol != null && SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, optionsSymbol);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,64 @@ void Method([OrchestrationTrigger] TaskOrchestrationContext context)
await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}

[Fact]
public async Task AccessingConfigurationIsNotAllowedInAzureFunctionsOrchestrations()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
[Function(""Run"")]
void Method([OrchestrationTrigger] TaskOrchestrationContext context, Microsoft.Extensions.Configuration.IConfiguration configuration)
{
_ = {|#0:configuration[""PATH""]|};
_ = {|#1:configuration.GetSection(""Section"")|};
}
");
string[] members = [
"IConfiguration.this[string]",
"IConfiguration.GetSection(string)",
];

DiagnosticResult[] expected = members.Select(
(member, i) => BuildDiagnostic().WithLocation(i).WithArguments("Method", member, "Run")).ToArray();

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}

[Fact]
public async Task AccessingOptionsIsNotAllowedInAzureFunctionsOrchestrations()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
class MyOptions
{
public string? Value { get; set; }
}

[Function(""Run"")]
void Method(
[OrchestrationTrigger] TaskOrchestrationContext context,
Microsoft.Extensions.Options.IOptions<MyOptions> options,
Microsoft.Extensions.Options.IOptionsSnapshot<MyOptions> snapshot,
Microsoft.Extensions.Options.IOptionsMonitor<MyOptions> monitor)
{
_ = {|#0:options.Value|};
_ = {|#1:snapshot.Value|};
_ = {|#2:monitor.CurrentValue|};
}
");

string[] members = [
"IOptions<Orchestrator.MyOptions>.Value",
"IOptions<Orchestrator.MyOptions>.Value",
"IOptionsMonitor<Orchestrator.MyOptions>.CurrentValue",
];

DiagnosticResult[] expected = members.Select(
(member, i) => BuildDiagnostic().WithLocation(i).WithArguments("Method", member, "Run")).ToArray();

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}

static DiagnosticResult BuildDiagnostic()
{
return VerifyCS.Diagnostic(EnvironmentOrchestrationAnalyzer.DiagnosticId);
}
}
}
Loading