Steps to Reproduce:
Expected Behavior:
Expanding that diagnostic ideally would display the stack trace instead of just the message.
Also if the diagnostic would contain a hotlink to the syntax which made that error occur would be a great help - currently it does not even display the file which prompted that exception.
Actual Behavior:

@taori What version of Visual Studio are you using? The features you have requested appear to be duplicates of #6696 and #6710, which were both implemented in #7917.
:memo: I proposed one of these features and argued strongly in favor of it. Before marking this as a duplicate, I want to make sure this is not a case where the feature is failing to work.
@sharwell Hmmm that is weird. My VS version is 15.5.4. I think as of right now that is the most recent version? Can you reproduce the issue?
@taori Can you show the Initialize method of the analyzer which is throwing this exception? (Even better would be a copy of the code for the analyzer, but I'm not sure if it's open source or not.)
Here's the test code which raises the exception:
[TestMethod]
public void DirectUdpClientNotMentioned()
{
var test = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public class SampleConsumer : DisposableBase2
{
private UdpClient _disposable;
}
}
";
var derivedSource1 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public abstract class DisposableBase1 : DisposableBase {}
}
";
var derivedSource2 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public abstract class DisposableBase2 : DisposableBase1 {}
}
";
var derivedSource3 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public class DisposableBase : IDisposable
{
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
}
}";
var expected = new[]
{
new DiagnosticResult
{
Id = DiagnosticIds.DisposableTargetAnalyzer.DisposableMemberNotMentionedInDisposeRule,
Message = string.Format(Solvum.Analyzers.Resources.DisposableMemberNotMentionedInDisposeMessageFormat, "_disposable"),
Severity = DiagnosticSeverity.Warning,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 10, 31)
}
},
};
VerifyCSharpDiagnostic(new []{test, derivedSource1, derivedSource2, derivedSource3 }, expected);
}
[TestMethod]
public void DirectUdpClientMentioned()
{
var test = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public class SampleConsumer : DisposableBase2
{
private UdpClient _disposable;
public override void Dispose(bool disposing) {
base.Dispose(disposing);
_disposable?.Dispose();
}
}
}
";
var derivedSource1 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public abstract class DisposableBase1 : DisposableBase {}
}
";
var derivedSource2 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public abstract class DisposableBase2 : DisposableBase1 {}
}
";
var derivedSource3 = @"
using System;
using System.Net.Sockets;
namespace ConsoleApplication1
{
public class DisposableBase : IDisposable
{
#region IDisposable Support
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}";
var expected = new DiagnosticResult[0];
VerifyCSharpDiagnostic(new []{test, derivedSource1, derivedSource2, derivedSource3 }, expected);
}
Analyzer code:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DisposableTargetAnalyzer : DiagnosticAnalyzer
{
// You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization
private static readonly DiagnosticDescriptor ClassWithDisposableMembersMustImplementDisposableRule =
new DiagnosticDescriptor(
id: DiagnosticIds.DisposableTargetAnalyzer.ClassWithDisposableMembersMustImplementDisposableRule,
title: new LocalizableResourceString(
nameof(Resources.ClassWithDisposableMembersMustImplementDisposableRuleTitle),
Resources.ResourceManager, typeof(Resources)),
messageFormat: new LocalizableResourceString(
nameof(Resources.ClassWithDisposableMembersMustImplementDisposableRuleMessageFormat),
Resources.ResourceManager, typeof(Resources)),
category: "Code Quality",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: new LocalizableResourceString(
nameof(Resources.ClassWithDisposableMembersMustImplementDisposableRuleDescription),
Resources.ResourceManager, typeof(Resources)));
private static readonly DiagnosticDescriptor DisposableMemberNotMentionedInDisposeRule =
new DiagnosticDescriptor(
id: DiagnosticIds.DisposableTargetAnalyzer.DisposableMemberNotMentionedInDisposeRule,
title: new LocalizableResourceString(
nameof(Resources.DisposableMemberNotMentionedInDisposeTitle),
Resources.ResourceManager, typeof(Resources)),
messageFormat: new LocalizableResourceString(
nameof(Resources.DisposableMemberNotMentionedInDisposeMessageFormat),
Resources.ResourceManager, typeof(Resources)),
category: "Code Quality",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: new LocalizableResourceString(
nameof(Resources.DisposableMemberNotMentionedInDisposeDescription),
Resources.ResourceManager, typeof(Resources)));
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return ImmutableArray.Create(
ClassWithDisposableMembersMustImplementDisposableRule
, DisposableMemberNotMentionedInDisposeRule
);
}
}
public override void Initialize(AnalysisContext context)
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Analyzer%20Actions%20Semantics.md for more information
context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration);
}
private void AnalyzeClass(SyntaxNodeAnalysisContext context)
{
try
{
if (context.Node is ClassDeclarationSyntax classDeclaration)
{
if (ClassImplementsIDisposable(context, classDeclaration, out var disposeMethod))
{
ReportDisposableMemberMentionErrors(context, classDeclaration, disposeMethod);
}
else
{
ReportMissingIDisposableImplementationOnClasses(context, classDeclaration);
}
}
}
catch (Exception e)
{
File.AppendAllText("D:\\roslynErrors.txt", e.ToString() + Environment.NewLine);
}
}
private IEnumerable<SyntaxNode> GetDisposableFieldNodes(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration)
{
foreach (var memberSyntax in classDeclaration.Members)
{
if (IsMemberDisposableField(context, memberSyntax, out var field))
foreach (var variable in field.Declaration.Variables)
{
yield return variable;
}
}
}
private void ReportMissingIDisposableImplementationOnClasses(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration)
{
var symbols = GetDisposableFieldSymbols(context, classDeclaration).ToImmutableArray();
if (symbols.Length > 0)
{
var namedSymbols = $"{string.Join(", ", symbols.Select(s => s.Name))}";
context.ReportDiagnostic(
Diagnostic.Create(
ClassWithDisposableMembersMustImplementDisposableRule,
classDeclaration.Identifier.GetLocation(), classDeclaration.Identifier.ValueText, namedSymbols));
}
}
private void ReportDisposableMemberMentionErrors(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax, ISymbol disposeMethod)
{
if (disposeMethod == null)
return;
var disposableFields = GetDisposableFieldSymbols(context, classDeclarationSyntax).ToImmutableArray();
if (disposableFields.Length == 0)
return;
var fieldsAndMethodsWithDisposeMentioning = GetDisposableFieldsCalledByMethods(context, classDeclarationSyntax).ToImmutableArray();
var mentioningByMethod = fieldsAndMethodsWithDisposeMentioning.ToLookup(d => d.methodSymbol);
var isFieldMentionedDictionary = disposableFields.ToDictionary(d => d, k => false);
var requiredPasses = isFieldMentionedDictionary.Count;
if (disposeMethod is IMethodSymbol disposableMethodSymbol)
{
var typeHierarchy = GetTypeSymbolHierarchy(context, classDeclarationSyntax).Reverse().ToImmutableArray();
var index = GetLowestImplementationIndex(typeHierarchy, disposableMethodSymbol.ContainingType);
var relevantTypes = typeHierarchy.Skip(index).ToImmutableArray();
if (relevantTypes.Length <= 0)
return;
var invocationHierarchy = GetComposedInvocationHierarchy(relevantTypes.Select(s => GetLocalInvocationHierarchy(context, s)).ToArray());
var reversedHierarchy = GetReversedHierarchy(invocationHierarchy);
var methodsInvokedByDispose = GetInvokedMethods(context, disposableMethodSymbol).Concat(new []{ disposableMethodSymbol});
foreach (var methodInvokedByBaseDispose in methodsInvokedByDispose)
{
var flatCallList = IterateCallTree(reversedHierarchy, methodInvokedByBaseDispose).ToImmutableArray();
foreach (var method in flatCallList)
{
var mentions = mentioningByMethod[method];
foreach (var mention in mentions)
{
if (mention.methodSymbol.Equals(method))
{
if (isFieldMentionedDictionary.TryGetValue(mention.fieldSymbol, out var passed) && !passed)
{
isFieldMentionedDictionary[mention.fieldSymbol] = true;
if (--requiredPasses == 0)
return;
}
}
}
}
}
foreach (var pair in isFieldMentionedDictionary)
{
if (!pair.Value)
{
foreach (var reference in pair.Key.DeclaringSyntaxReferences)
{
if (reference.GetSyntax() is VariableDeclaratorSyntax fieldDeclaratorSyntax)
{
var syntaxToken = fieldDeclaratorSyntax.Identifier;
context.ReportDiagnostic(Diagnostic.Create(DisposableMemberNotMentionedInDisposeRule, syntaxToken.GetLocation(), syntaxToken.ValueText));
}
if (reference.GetSyntax() is VariableDeclarationSyntax fieldDeclarationSyntax)
{
var syntaxToken = fieldDeclarationSyntax.Variables.First().Identifier;
context.ReportDiagnostic(Diagnostic.Create(DisposableMemberNotMentionedInDisposeRule, syntaxToken.GetLocation(), syntaxToken.ValueText));
}
}
}
}
}
}
private Dictionary<IMethodSymbol, HashSet<IMethodSymbol>> GetReversedHierarchy(Dictionary<IMethodSymbol, HashSet<IMethodSymbol>> invocationHierarchy)
{
var result = new Dictionary<IMethodSymbol, HashSet<IMethodSymbol>>();
foreach (var pair in invocationHierarchy)
{
foreach (var methodSymbol in pair.Value)
{
if(!result.TryGetValue(methodSymbol, out var set))
{
set = new HashSet<IMethodSymbol>();
result.Add(methodSymbol, set);
}
set.Add(pair.Key);
}
}
return result;
}
private IEnumerable<IMethodSymbol> IterateCallTree(Dictionary<IMethodSymbol, HashSet<IMethodSymbol>> invocationHierarchy, IMethodSymbol method)
{
yield return method;
if (invocationHierarchy.TryGetValue(method, out var calledMethods))
{
foreach (var methodSymbol in calledMethods)
{
foreach (var recursion in IterateCallTree(invocationHierarchy, methodSymbol))
{
yield return recursion;
}
}
}
}
private Dictionary<IMethodSymbol, HashSet<IMethodSymbol>> GetComposedInvocationHierarchy(params Dictionary<IMethodSymbol, ImmutableArray<IMethodSymbol>>[] invocationHierarchy)
{
var result = new Dictionary<IMethodSymbol, HashSet<IMethodSymbol>>();
foreach (var hierarchy in invocationHierarchy)
{
foreach (var pair in hierarchy)
{
if (!result.TryGetValue(pair.Key, out var set))
{
set = new HashSet<IMethodSymbol>();
result.Add(pair.Key, set);
}
foreach (var childMethod in pair.Value)
{
set.Add(childMethod);
}
}
}
return result;
}
private int GetLowestImplementationIndex(ImmutableArray<ITypeSymbol> items, INamedTypeSymbol containingType)
{
for (int i = 0; i < items.Length; i++)
{
if (items[i].Equals(containingType))
return i;
}
return -1;
}
private IEnumerable<ISymbol> GetDisposableFieldSymbols(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax)
{
foreach (var member in GetDisposableFieldNodes(context, classDeclarationSyntax))
{
var symbol = context.SemanticModel.GetDeclaredSymbol(member);
yield return symbol;
}
}
private Dictionary<IMethodSymbol, ImmutableArray<IMethodSymbol>> GetLocalInvocationHierarchy(SyntaxNodeAnalysisContext context, ITypeSymbol typeSymbol)
{
var result = new Dictionary<IMethodSymbol, ImmutableArray<IMethodSymbol>>();
foreach (var member in typeSymbol.GetMembers())
{
if (member is IMethodSymbol methodSymbol)
{
if (result.ContainsKey(methodSymbol))
continue;
var methods = GetInvokedMethods(context, methodSymbol).ToImmutableArray();
if (methods.Length > 0)
{
result.Add(methodSymbol, methods);
}
else
{
result.Add(methodSymbol, ImmutableArray<IMethodSymbol>.Empty);
}
}
}
return result;
}
private IEnumerable<IMethodSymbol> GetInvokedMethods(SyntaxNodeAnalysisContext context, IMethodSymbol methodSymbol)
{
if(methodSymbol == null || methodSymbol.DeclaringSyntaxReferences.Length == 0)
yield break;
if (methodSymbol.IsAbstract)
{
yield break;
}
foreach (var reference in methodSymbol.DeclaringSyntaxReferences)
{
var methodSyntax = reference.GetSyntax();
if (methodSyntax is MethodDeclarationSyntax methodDeclarationSyntax)
{
foreach (var symbol in methodDeclarationSyntax.Body.GetCalledMethodSymbols(context))
{
yield return symbol;
}
}
}
}
private IEnumerable<ITypeSymbol> GetTypeSymbolHierarchy(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax)
{
var baseSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
while (baseSymbol != null)
{
if (baseSymbol.SpecialType != SpecialType.System_Object)
yield return baseSymbol;
baseSymbol = baseSymbol.BaseType;
}
}
private IEnumerable<(IFieldSymbol fieldSymbol, IMethodSymbol methodSymbol)> GetDisposableFieldsCalledByMethods(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax)
{
foreach (var method in classDeclarationSyntax.Members.OfType<MethodDeclarationSyntax>())
{
var mentionedDisposables = GetMentionedDisposableFields(context, method);
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(method);
foreach (var fieldSymbol in mentionedDisposables)
{
yield return (fieldSymbol, methodSymbol);
}
}
}
private IEnumerable<IFieldSymbol> GetMentionedDisposableFields(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax method)
{
foreach (var fieldSymbol in method.Body.GetCalledFieldSymbols(context))
{
yield return fieldSymbol;
}
}
private static bool ClassImplementsIDisposable(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax, out ISymbol disposeMethodSymbol)
{
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is var classSymbol &&
TryFetchDisposeMethodRecursive(context, classSymbol, out disposeMethodSymbol))
return true;
disposeMethodSymbol = null;
return false;
}
private static bool TryFetchDisposeMethodRecursive(SyntaxNodeAnalysisContext context, INamedTypeSymbol currentClassSymbol, out ISymbol disposeMethodSymbol)
{
disposeMethodSymbol = null;
foreach (var @interface in currentClassSymbol.AllInterfaces)
{
if (@interface.SpecialType == SpecialType.System_IDisposable)
{
var members = @interface.GetMembers(nameof(IDisposable.Dispose));
if (members.Length == 0)
return false;
disposeMethodSymbol = currentClassSymbol.FindImplementationForInterfaceMember(members[0]);
return true;
}
}
if (currentClassSymbol.BaseType != null)
{
if (TryFetchDisposeMethodRecursive(context, currentClassSymbol.BaseType, out disposeMethodSymbol))
return true;
}
return false;
}
private bool IsMemberDisposableProperty(SyntaxNodeAnalysisContext context, MemberDeclarationSyntax member, out PropertyDeclarationSyntax propertySyntax)
{
if (member is PropertyDeclarationSyntax property)
{
propertySyntax = property;
var typeInfo = context.SemanticModel.GetTypeInfo(property.Type);
if (typeInfo.Type != null)
{
if (typeInfo.Type.SpecialType == SpecialType.System_IDisposable || typeInfo.Type.AllInterfaces.Any(d => d.SpecialType == SpecialType.System_IDisposable))
return true;
return false;
}
}
propertySyntax = null;
return false;
}
private bool IsMemberDisposableField(SyntaxNodeAnalysisContext context, MemberDeclarationSyntax member, out FieldDeclarationSyntax fieldSyntax)
{
if (member is FieldDeclarationSyntax field)
{
fieldSyntax = field;
var typeInfo = context.SemanticModel.GetTypeInfo(field.Declaration.Type);
if (typeInfo.Type != null)
{
if (typeInfo.Type.SpecialType == SpecialType.System_IDisposable || typeInfo.Type.AllInterfaces.Any(d => d.SpecialType == SpecialType.System_IDisposable))
return true;
return false;
}
}
fieldSyntax = null;
return false;
}
}
Extension classes:
public static class BlockSyntaxExtensions
{
private static IEnumerable<T> GetRecursion<T>(this SyntaxNode syntaxNode, SyntaxNodeAnalysisContext context, Func<SyntaxNode, bool> isOutput, Func<SyntaxNode, T> transform)
{
if(isOutput == null) throw new ArgumentNullException(nameof(isOutput));
if(transform == null) throw new ArgumentNullException(nameof(transform));
if (syntaxNode == null)
yield break;
if (isOutput(syntaxNode))
{
var transformation = transform(syntaxNode);
if (!EqualityComparer<T>.Default.Equals(transformation, default(T)))
yield return transformation;
}
if (syntaxNode is IfStatementSyntax ifStatement)
{
foreach (var fieldSymbol in GetRecursion(ifStatement.Statement, context, isOutput, transform))
{
yield return fieldSymbol;
}
foreach (var fieldSymbol in GetRecursion(ifStatement.Else, context, isOutput, transform))
{
yield return fieldSymbol;
}
}
if (syntaxNode is BlockSyntax blockSyntax)
{
foreach (var sourceStatement in blockSyntax.Statements)
{
foreach (var fieldSymbol in GetRecursion(sourceStatement, context, isOutput, transform))
{
yield return fieldSymbol;
}
}
}
}
public static IEnumerable<IFieldSymbol> GetCalledFieldSymbols(this BlockSyntax source, SyntaxNodeAnalysisContext context)
{
return source.GetRecursion(context, node => node is ExpressionStatementSyntax, node =>
{
if (node is ExpressionStatementSyntax expressionStatementSyntax)
{
if (expressionStatementSyntax.Expression is ConditionalAccessExpressionSyntax conditionalAccessExpressionSyntax)
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(conditionalAccessExpressionSyntax.Expression);
if (symbolInfo.Symbol is IFieldSymbol exists)
{
return exists;
}
}
if (expressionStatementSyntax.Expression is InvocationExpressionSyntax invocationExpressionSyntax)
{
if (invocationExpressionSyntax.Expression is MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccessExpressionSyntax.Expression);
if (symbolInfo.Symbol is IFieldSymbol exists)
{
return exists;
}
}
}
}
return null;
});
}
public static IEnumerable<IMethodSymbol> GetCalledMethodSymbols(this BlockSyntax source, SyntaxNodeAnalysisContext context)
{
return source.GetRecursion(context, d => d is ExpressionStatementSyntax, d =>
{
if (d is ExpressionStatementSyntax exStSyntax)
{
if (exStSyntax.Expression != null)
{
if (TryGetMethodSymbol(context, exStSyntax, out var methodSymbol))
return methodSymbol;
}
}
return null;
});
}
private static bool TryGetMethodSymbol(SyntaxNodeAnalysisContext context, ExpressionStatementSyntax expressionStatementSyntax, out IMethodSymbol methodSymbol)
{
if (expressionStatementSyntax.Expression is ConditionalAccessExpressionSyntax caeSyntax)
{
var methodFieldSymbolInfo = context.SemanticModel.GetSymbolInfo(caeSyntax.Expression);
if (methodFieldSymbolInfo.Symbol is IMethodSymbol calledMethodSymbol)
{
methodSymbol = calledMethodSymbol;
return true;
}
}
if (expressionStatementSyntax.Expression is InvocationExpressionSyntax iexSyntax)
{
var symbol = default(ISymbol);
try
{
symbol = context.SemanticModel.GetSymbolInfo(iexSyntax).Symbol;
}
catch (ArgumentException e)
{
symbol = context.Compilation.GetSemanticModel(iexSyntax.SyntaxTree).GetSymbolInfo(iexSyntax).Symbol;
}
if (symbol is IMethodSymbol calledInvocationMethodSymbol)
{
methodSymbol = calledInvocationMethodSymbol;
return true;
}
}
methodSymbol = null;
return false;
}
}
The marked line made both tests work, but figuring out what was wrong was very troublesome. If you strip the code within the catch block and remove the try/catch you'll be getting the NRE i was experiencing.
Hence the request to add more information in AD0001 diagnostic :)
I hope the provided information is sufficient to reproduce the issue.

I'm also observing this issue on Visual Studio 15.8.1. it seems that Visual Studio only shows the extended AD0001 message when the exception is encountered during Intellisense, but not during build.


Additionally I noticed that an .NET Framework analyzer (as opposed to .NET Standard) doesn't show up at all in the Intellisense errors. (There's two mostly-identical analyzers loaded in those screenshots. Notice how the "Intellisense Only" screenshot only has one exception message.) Not sure what is up with that.
I also should mention that with the analyzer where I initially found this issue, the output was truncated:

(Notice how there's no ending ' in the message. In fact, the message isn't complete either since there should be a Parameter name: Whatever in there too. I suspect this might be related to #1455.)
Looking in the output Window, it is truncated there as well. However, running csc directly (using the same command MSBuild used) reveals the whole error message:

(Unfortunately, still no stack trace to be seen.)
Debugging the compiler lead me to realize the reason the output from the compiler is missing the stack trace. The build/command line output only uses the diagnostic's message, but the stack trace is only present in the diagnostic's description. (This can be observed in AnalyzerExecutor.CreateAnalyzerExceptionDiagnostic.) Since DiagnosticFormatter.Format only uses the message, csc.exe does not print the stack trace.
It seems to me that at least one of these needs to happen:
csc.exe needs a new command line switch (/VerboseDiagnostics?) to enable a diagnostic formatter that prints the full description.csc.exe.@sharwell I'm willing to do a PR for either 1 or 2 if either seems like an acceptable solution to the Roslyn team.
It's worth noting that the experience with options 1 and 2 in Visual Studio will be subpar as long as #1455 is unfixed since these messages will always have multiple lines.