Как я могу определить, какие исключения могут быть сгенерированы данным методом?
Мой вопрос на самом деле такой же, как этот: "Узнайте, какие исключения может вызвать метод в C#". Однако мне бы очень хотелось узнать, знает ли кто-нибудь способ определения стека всех исключений, которые могут быть сгенерированы данным методом. Я надеюсь на инструмент или утилиту, которые я могу анализировать код во время компиляции или с помощью отражения, как FxCop, StyleCop или NCover. Мне не нужна эта информация во время выполнения, я просто хочу убедиться, что мы перехватываем исключения и правильно записываем их в наш код.
В настоящее время мы отслеживаем исключения, о которых знаем, и регистрируем все подстановочные знаки. Это работает хорошо; однако я просто надеялся, что кто-то использовал или знает инструмент, который может обнаружить эту информацию.
9 ответов
После моего предыдущего ответа мне удалось создать базовый искатель исключений. Он использует отражение на основе ILReader
класс, доступный здесь на блоге MSDN Хайбо Ло. (Просто добавьте ссылку на проект.)
Обновления:
- Теперь обрабатывает локальные переменные и стек.
- Правильно обнаруживает исключения, возвращаемые из вызовов методов или полей, а затем генерируемые.
- Теперь обрабатывает стек толкает / выдвигает полностью и подходящим образом.
Вот код, в полном объеме. Вы просто хотите использовать GetAllExceptions(MethodBase)
метод либо как расширение, либо как статический метод.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using ClrTest.Reflection;
public static class ExceptionAnalyser
{
public static ReadOnlyCollection<Type> GetAllExceptions(this MethodBase method)
{
var exceptionTypes = new HashSet<Type>();
var visitedMethods = new HashSet<MethodBase>();
var localVars = new Type[ushort.MaxValue];
var stack = new Stack<Type>();
GetAllExceptions(method, exceptionTypes, visitedMethods, localVars, stack, 0);
return exceptionTypes.ToList().AsReadOnly();
}
public static void GetAllExceptions(MethodBase method, HashSet<Type> exceptionTypes,
HashSet<MethodBase> visitedMethods, Type[] localVars, Stack<Type> stack, int depth)
{
var ilReader = new ILReader(method);
var allInstructions = ilReader.ToArray();
ILInstruction instruction;
for (int i = 0; i < allInstructions.Length; i++)
{
instruction = allInstructions[i];
if (instruction is InlineMethodInstruction)
{
var methodInstruction = (InlineMethodInstruction)instruction;
if (!visitedMethods.Contains(methodInstruction.Method))
{
visitedMethods.Add(methodInstruction.Method);
GetAllExceptions(methodInstruction.Method, exceptionTypes, visitedMethods,
localVars, stack, depth + 1);
}
var curMethod = methodInstruction.Method;
if (curMethod is ConstructorInfo)
stack.Push(((ConstructorInfo)curMethod).DeclaringType);
else if (method is MethodInfo)
stack.Push(((MethodInfo)curMethod).ReturnParameter.ParameterType);
}
else if (instruction is InlineFieldInstruction)
{
var fieldInstruction = (InlineFieldInstruction)instruction;
stack.Push(fieldInstruction.Field.FieldType);
}
else if (instruction is ShortInlineBrTargetInstruction)
{
}
else if (instruction is InlineBrTargetInstruction)
{
}
else
{
switch (instruction.OpCode.Value)
{
// ld*
case 0x06:
stack.Push(localVars[0]);
break;
case 0x07:
stack.Push(localVars[1]);
break;
case 0x08:
stack.Push(localVars[2]);
break;
case 0x09:
stack.Push(localVars[3]);
break;
case 0x11:
{
var index = (ushort)allInstructions[i + 1].OpCode.Value;
stack.Push(localVars[index]);
break;
}
// st*
case 0x0A:
localVars[0] = stack.Pop();
break;
case 0x0B:
localVars[1] = stack.Pop();
break;
case 0x0C:
localVars[2] = stack.Pop();
break;
case 0x0D:
localVars[3] = stack.Pop();
break;
case 0x13:
{
var index = (ushort)allInstructions[i + 1].OpCode.Value;
localVars[index] = stack.Pop();
break;
}
// throw
case 0x7A:
if (stack.Peek() == null)
break;
if (!typeof(Exception).IsAssignableFrom(stack.Peek()))
{
//var ops = allInstructions.Select(f => f.OpCode).ToArray();
//break;
}
exceptionTypes.Add(stack.Pop());
break;
default:
switch (instruction.OpCode.StackBehaviourPop)
{
case StackBehaviour.Pop0:
break;
case StackBehaviour.Pop1:
case StackBehaviour.Popi:
case StackBehaviour.Popref:
case StackBehaviour.Varpop:
stack.Pop();
break;
case StackBehaviour.Pop1_pop1:
case StackBehaviour.Popi_pop1:
case StackBehaviour.Popi_popi:
case StackBehaviour.Popi_popi8:
case StackBehaviour.Popi_popr4:
case StackBehaviour.Popi_popr8:
case StackBehaviour.Popref_pop1:
case StackBehaviour.Popref_popi:
stack.Pop();
stack.Pop();
break;
case StackBehaviour.Popref_popi_pop1:
case StackBehaviour.Popref_popi_popi:
case StackBehaviour.Popref_popi_popi8:
case StackBehaviour.Popref_popi_popr4:
case StackBehaviour.Popref_popi_popr8:
case StackBehaviour.Popref_popi_popref:
stack.Pop();
stack.Pop();
stack.Pop();
break;
}
switch (instruction.OpCode.StackBehaviourPush)
{
case StackBehaviour.Push0:
break;
case StackBehaviour.Push1:
case StackBehaviour.Pushi:
case StackBehaviour.Pushi8:
case StackBehaviour.Pushr4:
case StackBehaviour.Pushr8:
case StackBehaviour.Pushref:
case StackBehaviour.Varpush:
stack.Push(null);
break;
case StackBehaviour.Push1_push1:
stack.Push(null);
stack.Push(null);
break;
}
break;
}
}
}
}
}
Подводя итог, этот алгоритм рекурсивно перечисляет (сначала в глубину) любые методы, вызываемые в указанном, путем чтения инструкций CIL (а также отслеживания уже посещенных методов). Он поддерживает единый список коллекций, которые могут быть выброшены с помощью HashSet<T>
объект, который возвращается в конце. Кроме того, он поддерживает массив локальных переменных и стек, чтобы отслеживать исключения, которые не генерируются сразу после их создания.
Конечно, этот код не является непогрешимым в его текущем состоянии. Есть несколько улучшений, которые нужно сделать, чтобы он был устойчивым, а именно:
Обнаружение исключений, которые не генерируются напрямую, с помощью конструктора исключений.(т.е. исключение извлекается из локальной переменной или вызова метода.)Исключения поддержки выталкиваются из стека, а затем снова включаются.- Добавьте обнаружение контроля потока. Блоки try-catch, которые обрабатывают любое выброшенное исключение, должны удалить соответствующее исключение из списка, если только
rethrow
Инструкция обнаружена.
Кроме того, я считаю, что код достаточно полный. Может потребоваться немного больше расследования, прежде чем я пойму, как именно выполнить обнаружение управления потоком (хотя я полагаю, что теперь я могу видеть, как оно работает на уровне IL).
Эти функции, вероятно, можно было бы превратить в целую библиотеку, если бы нужно было создать полнофункциональный "анализатор исключений", но, надеюсь, это, по крайней мере, обеспечит надежную отправную точку для такого инструмента, если он еще недостаточно хорош в его текущем состоянии.
Во всяком случае, надеюсь, что это помогает!
Это не должно быть чрезвычайно сложно. Вы можете получить список исключений, созданных методом, подобным этому:
IEnumerable<TypeReference> GetCreatedExceptions(MethodDefinition method)
{
return method.GetInstructions()
.Where(i => i.OpCode == OpCodes.Newobj)
.Select(i => ((MemberReference) i.Operand).DeclaringType)
.Where(tr => tr.Name.EndsWith("Exception"))
.Distinct();
}
Фрагменты используют Lokad.Quality.dll из общих библиотек Lokad с открытым исходным кодом (который использует Mono.Cecil для выполнения тяжелой работы по отражению кода). Я фактически поместил этот код в один из тестовых случаев в транке.
Скажем, у нас есть такой класс:
class ExceptionClass
{
public void Run()
{
InnerCall();
throw new NotSupportedException();
}
void InnerCall()
{
throw new NotImplementedException();
}
}
затем, чтобы получить все исключения только из метода Run:
var codebase = new Codebase("Lokad.Quality.Test.dll");
var type = codebase.Find<ExceptionClass>();
var method = type.GetMethods().First(md => md.Name == "Run");
var exceptions = GetCreatedExceptions(method)
.ToArray();
Assert.AreEqual(1, exceptions.Length);
Assert.AreEqual("NotSupportedException", exceptions[0].Name);
Теперь остается только пройти вниз по стеку вызовов методов до определенной глубины. Вы можете получить список методов, на которые ссылается такой метод:
var references = method.GetReferencedMethods();
Теперь, прежде чем мы сможем вызывать GetCreatedExceptions для любого метода в стеке, нам нужно просто посмотреть на кодовую базу и преобразовать все экземпляры MethodReference в экземпляры MethodDefinition, фактически содержащие байт-код (с некоторым кэшированием, чтобы избежать сканирования существующих ветвей). Это самая трудоемкая часть кода (поскольку объект Codebase не реализует поиск методов поверх Cecil), но это должно быть выполнимо.
Этот ответ был опубликован в другом вопросе, на который вы ссылаетесь, и я знаю, что рекомендовал его ранее в другом подобном вопросе. Вы должны попробовать Exception Hunter. В нем перечислены все исключения, которые могут быть выброшены. Когда я впервые запустил его в своем коде, я был очень удивлен размером этого списка даже для простых функций. Существует 30-дневная пробная версия бесплатно, поэтому нет никаких причин, чтобы не попробовать.
Я очень сомневаюсь, что есть какой-то (по крайней мере, прямой) способ сделать это в C#. Сказав это, у меня есть идея, которая может сработать, так что читайте дальше, пожалуйста...
Во-первых, стоит отметить, что проведение перебора с использованием огромного количества перестановок аргументов явно неосуществимо. Даже имея предварительные знания о типах параметров (что я не считаю желательным в вашей ситуации), задача в основном сводится к проблеме остановки в общем случае, так как вы не знаете, что функция прекратит заданные определенные параметры. В идеале исключения должны остановить это, но это не всегда так, конечно.
Теперь, пожалуй, самый надежный метод - это анализ самого исходного кода (или более реалистичного кода CIL), чтобы увидеть, какие исключения могут быть выброшены. Я верю, что это может быть реально осуществимо. Простой алгоритм может выглядеть примерно так:
- Выполните поиск данного метода по глубине или по ширине. Найдите все методы / свойства, которые вызываются в любом месте в теле метода, и используйте их.
- Для каждого блока кода CIL в дереве методов / свойств проверьте код на наличие исключений и добавьте его в список.
Это даже позволит вам получить подробную информацию об исключениях, например, были ли они выброшены непосредственно вызываемым методом, или глубже в стеке вызовов, или даже сами сообщения об исключениях. Как бы то ни было, я расскажу о том, чтобы попытаться реализовать это позже во второй половине дня, поэтому я дам вам знать, насколько осуществима эта идея.
Моя методология для ситуации такого типа состоит в том, чтобы обработать все исключения, которые я хочу, и затем переопределить событие UnhandledException приложения, чтобы регистрировать любые другие, о которых я не знаю. Тогда, если я столкнусь с чем-то, что, я думаю, я мог бы решить, то я просто обновил бы соответственно.
Надеюсь, это поможет!
В отличие от Java C# не имеет понятия проверенных исключений.
На макроуровне вы должны поймать все и войти или сообщить пользователю об ошибке. Когда вы знаете о конкретных исключениях, метод может быть вызван, а затем определенно обрабатывает это соответствующим образом, но обязательно разрешите всплыть (предпочтительно) любым другим исключениям или зарегистрируйте их, в противном случае у вас будут ошибки, которые вы не сможете найти и, как правило, создадите жизнь. несчастный для любого нанятого, чтобы помочь уменьшить список ошибок - был там, не весело!:)
У Джона Роббинса была серия статей по созданию правил FxCop, включая статью MSDN, в которой указывалось, какие исключения были выброшены. Это должно было предупредить об отсутствии XML-документации для исключений, но идея была бы той же.
Я написал надстройку для Reflector под названием ExceptionFinder, которая обрабатывает это. Вы можете получить его по адресу:
http://exfinderreflector.codeplex.com/
С уважением, Джейсон
Это не столько ответ, сколько построение поверх большой работы, проделанной @Noldorin выше. Я использовал приведенный выше код и подумал, что было бы очень полезно иметь инструмент, который разработчик мог бы указать на произвольную сборку / dll и увидеть список исключений.
Основываясь на вышеупомянутой работе, я создал инструмент, который делает именно это. Я поделился источником на GitHub для всех, кто заинтересован. Это очень просто, у меня было всего пару часов на то, чтобы написать его, но не стесняйтесь раскошелиться и делать обновления, если считаете нужным...