Опыт кастомного анализа c# кода

в 10:18, , рубрики: .net, fxcop, Visual Studio, анализ кода, отладка, Отладчик, Совершенный код, метки: , , , ,

Довольно давно я делился проблемой тестирования кода в финализаторе и недавно жаловался на падение теста (Как тестировать код финализатора (c#) и Как тестировать код финализатора (c#). Послесловие: тест все-таки упал).
В ходе обсуждения комрадом withkittens была высказана идея:

Финализатор (при правильной реализации IDisposable pattern) должен (should) вызывать Dispose(false). Этот факт можно тестировать статическим анализом. Соответственно, если Dispose(false) вызывает удаление файла (вы же написали тест?), то можно быть уверенным, что и финализатор тоже вызовет удаление файла, unit-тест излишен.

Мне эта идея показалась очень здравой, кроме того, иногда хочется контролировать исходный код более кастомно, чем дает встроенный анализ кода или решарпер.
Опыт реализации кастомных правил анализа кода под катом + «как ozcode помог в процессе исследования внешней библиотеки»

Прежде всего я расширил список вещей которые я хотел бы искать:

Проверка поля определенного типа

Решил начать с самого простого: объект лог, если он объявлен как поле некого класса должен быть статическим, как советуют на сайте nlog.
Таким образом первая строчка подходит под правило, а вторая нет:

private static ILog m_logger1 = LogManager.GetInstance();
private ILog m_logger2 = LogManager.GetInstance();

Финализатор должен вызывать «Dispose(false)»

Действительно, что бы не писать модульный тест на финализатор, достаточно убедиться, что он вызывает Dispose(false), который тестируется отдельно.

Реализованный сервис регистрируется в менеджере сервисов

Один из коллег указал на то, что иногда человек из одной команды реализует некий сервис, тестирует его, но забывает зарегистрировать его в менеджере сервисов, а потом человек из другой команды пытается использовать этот сервис и получает ошибку, что сервис не найден.


Таким образом, я поставил себе задачу релизовать через какие-то существующие инструменты статического анализа кода свои собственные правила. Первым инструментом, который мне посоветовал все тот же withkittens оказался стандартный встроенный Code Analysis.

Как оказалось, порог вхождения в написание своего правила очень низкий. Достаточно прочитать хотя бы пост How to write custom static code analysis rules and integrate them into Visual Studio 2010. Пост описывает для VS2010, но я реализовывал на VS2013 и не встретил каких-то особых проблем. Надо отметить, что есть как минимум одна описка в этом посте, и, что более интересно, у меня не получилось нормально интегрировать мои правила в саму студию: правило то показывается, то не показывается (меня это напрягает, но не сильно, потому что я все равно собираюсь запускать правила как часть тестов из командной строки).

Итак, пошаговая инструкция есть тут, поэтому я начну с реализации моих правил (исходники на гитхабе):

Проверка поля определенного типа

Очень просто: реализуется виртуальный метод Check(Member member), который получает member (гугл-переводчик переводит member как «член», но я не стану рисковать), проверяем, что этот member есть поле класса (as Field), проверяем, что это поле есть наш лог (тупо по имени типа), и проверяем, что это поле статическое или нет. Если нет, то создаем «new Problem» и вставляем в список Problems.

        public override ProblemCollection Check(Member member)
        {
            Field field = member as Field;
            if (field == null)
            {
                // This rule only applies to fields.
                // Return a null ProblemCollection so no violations are reported for this member.
                return null;
            }

            string actualType = field.Type.FullName;
            if (actualType == "SamplesForCodeAnalysis.ILog")
            {
                if (!field.IsStatic)
                {
                    Resolution resolution = GetResolution(field, actualType);
                    Problem problem = new Problem(resolution);
                    Problems.Add(problem);
                }
            }

            return Problems;
        }

Финализатор должен вызывать «Dispose(false)»

Эта задача более интересна, потому что требует проверить целый метод. Я ее реализовал очень частично, чисто попробовать, таким образом:

Начинаем как обычно: реализуется виртуальный метод Check(Member member), который получает member, проверяем, что этот member есть метод (as Method), проверяем, что это финализатор (тупо по окончанию метода на ".Finalize"). Далее берем список инструкций этого метода «method.Instruction» и ищем, а есть ли хотя бы одна инструкция, которая заканчивается на ".Dispose(System.Boolean)". Это конечно не совсем то, что надо, но хоть наличие факта отсутствия вызова Dispose это правило отловит.

        public override ProblemCollection Check(Member member)
        {
            var method = member as Method;
            if (method == null)
            {
                // This rule only applies to fields.
                // Return a null ProblemCollection so no violations are reported for this member.
                return null;
            }

            if (method.FullName.EndsWith(".Finalize"))
            {
                bool disposeFound = method.Instructions.Any(p => p.Value is Method && (p.Value as Method).FullName.EndsWith(".Dispose(System.Boolean)"));
                if (!disposeFound)
                {
                    Resolution resolution = GetResolution(method);
                    var problem = new Problem(resolution);
                    Problems.Add(problem);
                }
                return Problems;
            }

            return Problems;
        }

В примере, я реализовал такой финализатор:

       ~Samples()
        {
            Dispose(true);
            int i = 0;
            i++;
            int k = 56;
            Dispose(false);
        }

и получил такой список инструкций и пока не понял как проверить, что есть только один вызов Dispose и причем с параметром False:
Опыт кастомного анализа c# кода
Так что тут еще есть над чем поработать, но отловить отсуствие вызова Dispose() уже можно.

Реализованный сервис регистрируется в менеджере сервисов

Тут задача, еще более интересная: есть класс-сервис (этот такой класс, который наследуется от специального интерфейса):

    public class UserService1 : IBaseService
    {        
    }

    public class UserService2 : IBaseService
    {        
    }

Эти сервисы должны быть зарегистрированы в менеджере:

    public class ServiceManager
    {
        public IDictionary<string, IBaseService> AllServices = new Dictionary<string, IBaseService>();
        public void RegisterAllServices()
        {
            AllServices.Add("service1", new UserService1());
        }
    }

Как же споймать ситуацию, что сервис UserService2 не был зарегистрирован.
Реализуем такое правило (с учетом того, что в данном случае, сервисы и менеджер находятся в одном проекте, если в разых — не пробовал):

Реализуем немного другой виртуальный метод «Check(TypeNode typeNode)», проверяем прежде всего, что тип наследуется от нужного интефейса (в нашем случае от ".IBaseService"), далее ищем менеджер сервисов через «DeclaringModule.Types» по имени ".ServiceManager". В найденном типе ищем нужный метод ".RegisterAllServices" и наконец находим (или не находим) изначальный тип, который сервис "(.Contains(typeNode.FullName + "("))". Согласен, что не совсем чисто, но опять же, в случае отсутствия регистрации, правило сработает и предупреждение будет получено.

        public override ProblemCollection Check(TypeNode typeNode)
        {
            if (typeNode.Interfaces.Any())
            {
                InterfaceNode foundServiceInterface = typeNode.Interfaces.First(i => i.FullName.EndsWith(".IBaseService"));
                if (foundServiceInterface!=null)
                {
                    bool foundUsage = false;
                    TypeNode serviceManagerTypeNode = foundServiceInterface.DeclaringModule.Types.First(t => t.FullName.EndsWith(".ServiceManager"));
                    if (serviceManagerTypeNode != null)
                    {
                        Member member = serviceManagerTypeNode.Members.First(t => t.FullName.EndsWith(".RegisterAllServices"));
                        var method = member as Method;
                        if (method != null)
                        {
                            foundUsage = method.Instructions.Any(opcode => opcode.Value != null && opcode.Value.ToString().Contains(typeNode.FullName + "("));
                        }
                    }

                    if (!foundUsage)
                    {
                        Resolution resolution = GetResolution(typeNode.FullName);
                        var problem = new Problem(resolution);
                        Problems.Add(problem);
                    }
                }
            }
            return Problems;
        }

Хотелось бы отдельно отметить как я изучал типы библиотеки FxCopSdk. Не подумайте плохо, документацию я не читал (и даже не искал).
Я воспользовался OzCode (расширение для Visual Sudio).
Например, вместо того, что бы капаться в отладчике и искать как в этом объекте найти ссылку (вернее даже список ссылок) на нужный мне объект, я просто запустил поиск «а найди мне как от этого объекта добраться до ServiceManager»:
Опыт кастомного анализа c# кода
Ok, значит у «типа» есть список интерфейсов, а у интерфейса есть DeclaringModule, а у него есть список типов, один из которых ServiceManager.
Если бы ozcode еще бы и код генерил для типичных задач поиска!

Также я пользовался возможностью reveal, которая позволила мне увидеть весь список в том виде, в котором мне хотелось:
Опыт кастомного анализа c# кода

Исходники загружены на гитхаб github.com/constructor-igor/UserDefinedCodeAnalysis
В папке Samples я положил файл sample.cmd, который запускает анализ этих правил на примере (конечно нужно все сперва скомпилировать).
После запуска этого файла генерируется файла-отчет results.xml, который содержит примерно такие результаты:

        <Type Name="LogStaticFieldSamples" Kind="Class" Accessibility="Public" ExternallyVisible="True">
         <Members>
          <Member Name="#m_logger2" Kind="Field" Static="False" Accessibility="Private" ExternallyVisible="False">
           <Messages>
            <Message TypeName="EnforceStaticLogger" Category="MyRules" CheckId="CR1001" Status="Active" Created="2014-06-25 09:11:55Z" FixCategory="NonBreaking">
             <Issue Certainty="101" Level="Warning">Field 'LogStaticFieldSamples.m_logger2' recommended be static, because type SamplesForCodeAnalysis.ILog.</Issue>
            </Message>
           </Messages>
          </Member>
         </Members>
        </Type>

Автор: constructor

Источник

Поделиться

* - обязательные к заполнению поля