BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Policjant na sterydach - statyczna analiza kodu przy użyciu FxCop’a

31-05-2005 18:14 | robert.wilczynski
Artykuł poświęcony jest zagadnieniu statycznej analizy aplikacji i bibliotek napisanych na platformę .Net z wykorzystaniem narzędzia FxCop. Jako, że nie jest to wprowadzenie do samego narzędzia, a raczej przedstawienie możliwości jego rozbudowy, wskazane (aczkolwiek nie konieczne) jest wcześniejsze doświadczenie w pracy z FxCop’em, oraz podstawowa znajomość języka C#. Po zapoznaniu się z mechanizmem działania samego narzędzia, czytelnik dowie się jak rozszerzyć jego możliwości poprzez własne bib

Wstęp

 

Na obecnym etapie rozwoju inżynierii oprogramowania niemożliwe jest pisanie programów, które pozbawione są błędów. Możliwe (i wskazane) jest natomiast dążenie do tego, aby programy posiadały tych błędów jak najmniej.  Zapewnienie odpowiedniej jakości kodu, czy to poprzez stosowanie metodologii Extreme Programming (a w szczególności pair programming), okresowych rewizji kodu (code reviews), czy też kombinacji różnych podejść jest niestety procesem kosztownym. Automatyzacja tego procesu i wplecenie go w cykl życia aplikacji, pomimo iż nie wyeliminuje do końca ingerencji człowieka i samych błędów, zdecydowanie usprawnia proces i zwiększa efektywność pracy całego zespołu. Tu przychodzą nam z pomocą narzędzia do statycznej analizy kodu, a wśród nich bohater tego artykułu - FxCop.

 

Statyczna analiza kodu

 

Tym określeniem przyjęło się nazywać proces, w którym narzędzia na podstawie dostarczonych reguł analizują pliki źródłowe w celu wychwycenia potencjalnych problemów związanych z kodem aplikacji.

W przeciwieństwie do dynamicznej analizy, podczas której weryfikacji podlega skompilowany i uruchomiony fragment kodu (lub cała aplikacja), analiza statyczna nie wymaga uruchamiania napisanego programu. Nie da się jednoznacznie stwierdzić do której grupy analizatorów należy FxCop. Co prawda analizowane aplikacje (lub biblioteki) nie są uruchamiane, jednak sam proces nie ma nic wspólnego z analizą źródła.

 

Jak działa FxCop?

 

Koncepcja statycznej analizy kodu (w pierwotnym rozumieniu tego terminu) jest rozwiązaniem trudnym do zaimplementowania. Należy, na przykład, przewidzieć sytuacje, w których fragment kodu odpowiedzialny za tę samą logikę mógł być napisany na wiele sposobów. Pomimo tego, że kompilator C# również zapisuje kod do języka pośredniego (Intermediate Language – IL) na różne sposoby w zależności od użytej składni, kody operacji IL (OpCode) są bardziej jednoznaczne, niż źródło programu i co ważniejsze – niezależne od wybranego języka programowania. Dzięki temu raz napisany analizator bez problemu obsługuje wszystkie języki dostępne dla .Net Framework.

 

Przedmiotem analizy FxCop’a są metadane, ciała metod zapisane jako instrukcje IL, oraz grafy wywołań. Poprzednie wersje FxCop’a analizowały podzespoły (ang. assembly) używając mechanizmu refleksji dostępnego w .Net. Najnowsza wersja (1.312) używa do tego celu silnika introspekcji (Introspection Engine - IE), który korzysta z  własnego analizatora metadanych, co umożliwia pracę z analizowaną biblioteką bez konieczności zamykania FxCop’a. Wcześniej analizowany plik był blokowany co wynikało bezpośrednio z implementacji mechanizmu dynamicznego ładowania podzespołów w czasie wykonania. Wiązało się to z koniecznością zamknięcia FxCop’a w celu kompilacji biblioteki przed powtórną analizą. IE pozwala również na analizę bibliotek skompilowanych inną wersją .Net Framework niż ta, na której uruchomiony jest FxCop. Najważniejszą jednak, z punku widzenia elastyczności, cechą jest rozszerzona możliwość analizy operacji wykonywanych wewnątrz metod – funkcjonalność, która nie jest w łatwy sposób dostępna przy użyciu mechanizmu refleksji.

 

Reguły

 

Samo narzędzie byłoby oczywiście bezużyteczne gdyby nie dostarczone wraz z nim reguły. Twórcy FxCop’a podzielili ponad dwieście reguł dostarczonych z programem na kilka kategorii :

 

Grupa 

Zakres analizy          

Design

Poprawność architektury bibliotek pod kątem zgodności z zasadami nakreślonymi w dokumencie Design Guidelines for Class Library Developers [1].

Globalization

Globalizacja czyli gotowość aplikacji do lokalizacji.

Interoperability

Współpraca z kodem niezarządzanym (głównie COM).

Maintainability

Konserwacja kodu.

Mobility

Efektywne zarządzanie energią.

Naming

Zgodność z konwencjami nazewniczymi nakreślonymi w dokumencie Design Guidelines for Class Library Developers [1].

Performance

Wydajność kodu.

Portability

Przenaszalność na inne platformy.

Security

Bezpieczeństwo.

Usage 

Poprawne użycie .Net Framework.

 

Poziom istotności (MessageLevel), współczynnik pewności (Certainty) i kategorie napraw (FixCategories)

 

Każdej skojarzonej z regułą wiadomości przypisany jest poziom istotności (MessageLevel), oraz współczynnik pewności (Certainty), który określa prawdopodobieństwo poprawnej identyfikacji problemu. Poziom istotności powinien być wybrany uwzględniając oczywistość napotkanego problemu, oraz ryzyko związane z jego zignorowaniem.

 

Każdej regule musi zostać przypisany jeden z pięciu dostępnych poziomów istotności:

 

·         Critical Error (Błąd krytyczny) / Error (Błąd) - Wiadomości, którym nadano ten poziom dotyczą problemów, które mogą zagrozić stabilności kodu. Powinny być one rozwiązane w pierwszej kolejności, zaś decyzja o ewentualnym zignorowaniu takiego problemu dokładnie przemyślana.

 

·         Critical Warning (Ostrzeżenie krytyczne) / Warning (Ostrzeżenie) - Ta grupa problemów zwykle nie dotyczy sytuacji, które mogłyby zagrozić stabilności aplikacji, jednak powinny zostać starannie przeanalizowane pod kątem optymalności dokonanych przez programistę wyborów.

 

·         Informational (Informacja) - Stosowany do wiadomości, które dostarczają jedynie informacji o przedmiocie analizy, nie identyfikują zaś potencjalnych problemów z nim związanych.

 

Współczynnik pewności określa prawdopodobieństwo, z jakim problem został poprawnie zidentyfikowany. Podobnie jak poziom istotności jest to wartość określana subiektywnie przez autora reguły, jednak powinna zależeć od algorytmu użytego do wykrycia problemu, oraz cech specyficznych dla problemu, które nie mogą zostać zweryfikowane na podstawie statycznej analizy. Współczynnik pewności określany jest w procentach (0-99%). Im większa wartość tym większe prawdopodobieństwo, że dana reguła poprawnie identyfikuje problem. Zaniedbywanie reguł o niskim współczynniku pewności jest poważnym błędem. Niejednokrotnie identyfikują one istotne problemy, które ze względu na swą naturę i ograniczone możliwości analizy statycznej nie mogą być wykrywane z dużą pewnością.

 

Dodatkowo w najnowszej wersji FxCop’a dodano nowy właściwość reguły - FixCategories. Określa ona ewentualny wpływ rozwiązania zidentyfikowanego problemu na resztę kodu biblioteki. Na przykład zmiana nazwy członka klasy pociąga za sobą konieczność zmian we wszystkich miejscach, w których się do niego odwołujemy, natomiast usunięcie nieużywanych nigdzie prywatnych metod nie ma wpływu na pozostały kod. W tym celu określono trzy kategorie napraw:

 

·         Breaking - Wprowadzenie sugerowanych zmian pociąga za sobą konieczność modyfikacji innych partii kodu.

 

·         NonBreaking - Wprowadzone zmiany nie będą wymagały dodatkowych modyfikacji.

 

·         DependsOnFix - Dodatkowe modyfikacje mogą być niezbędne w zależności od rozwiązania, które wybierzemy.

 

Dozbrajamy FxCop’a

 

Każda reguła jest oddzielną klasą, zatem pisanie biblioteki reguł zaczniemy od utworzenia nowe projektu biblioteki klas. Do nowo utworzonego projektu dodać należy odpowiednie referencje do bibliotek zawierających klasy, które wykorzystamy do stworzenia naszych reguł:

 

·         FxCopSdk.dll

·         Microsoft.Cci.dll

 

Oba pliki można znaleźć w katalogu, w którym zainstalowany został FxCop. Należy również zadeklarować odpowiednie przestrzenie nazw w plikach, w których umieścimy nasze reguły:

 

·         Microsoft.Cci

·         Microsoft.Tools.FxCop.Sdk

·         Microsoft.Tools.FxCop.Sdk.Introspection

 

Kolejnym etapem powinno być stworzenie abstrakcyjnej klasy dziedziczącej z BaseIntrospectionRule (Microsoft.Tools.FxCop.Sdk.Introspection) z której z kolei będą dziedziczyły wszystkie reguły należące do nowej biblioteki. Korzyści płynące z wprowadzenia dodatkowego poziomu abstrakcji staną się oczywiste po spojrzeniu na konstruktor klasy BaseIntrospectionRule.

 

protected BaseIntrospectionRule(string name, string resourceName, Assembly resourceAssembly);

 

·         name – nazwa reguły – zwykle taka sama jak nazwa klasy

·         resourceName – nazwa pliku XML, w którym znajduje się opis reguły

·         resourceAssembly – podzespół, w którym znajduje się wspomniany plik.

 

Najczęściej jedynym parametrem, który będzie ulegał zmianie w zależności od reguły jest jej nazwa. Twórcy reguł dostarczonych z FxCop’em przechowują opisy wszystkich reguł w pojedynczym pliku dołączonym do podzespołu z regułami jako zasób osadzony (embedded resource). Zdecydowanie się na przyjęcie takiej konwencji oszczędzi nam przepisywania długiego konstruktora dla każdej reguły dołączonej do naszej biblioteki. Pozwala to również na zmianę nazwy pliku opisującego reguły tylko w jednym miejscu, nie zaś w konstruktorze każdej dodanej reguły, oraz dodanie funkcjonalności wspólnej dla wszystkich reguł danej grupy.

 

Using Microsoft.Tools.FxCop.Sdk.Introspection;

namespace NetSolutions.FxCop.Rules.Naming

{

            public abstract class NamingIntrospectionRule : BaseIntrospectionRule

            {

                        protected NamingIntrospectionRule(string name)

                                   : base(name, "NetSolutions.FxCop.Rules.Naming.NamingRules",

                                                                         typeof(NamingIntrospectionRule).Assembly)

                        {

                                   /* */

                        }

            }

}

C#, NamingIntrospectionRule.cs

 

Tak powinno wyglądać rozpoczęcie pracy nad każdą nową biblioteką reguł. Następny krok to dodanie pliku XML opisującego reguły i stworzenie konkretnych klas. Poniżej przykładowy plik opisujący regułę, której implementacją zajmiemy się w dalszej części artykułu oraz opis elementów i atrybutów, z których się składa.

 

 

<?xml version="1.0" encoding="utf-8"?>

<Rules FriendlyName="Konwencje nazewnicze dla prywatnych pól.">

    <Rule TypeName="PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore" Category="NetSolutions.Naming" CheckId="NS9999">

        <Name>Nazwy identyfikatorów prywatnych pól powinny być poprzedzone prefiksem 'm_'.</Name>

        <Description>Nazwy identyfikatorów prywatnych pól powinny być poprzedzone prefiksem 'm_'.</Description>

        <Resolution Name="Field">Zmień nazwę prywatnego pola tak, aby odpowiadała przyjętym standardom. Nazwa {0} powinna zostać zastąpiona nazwą {1}.</Resolution>       

        <Url>http://dotnetpb.aspweb.cz/fxcop/</Url>

        <Email>coding.net@gmail.com</Email>

        <MessageLevel Certainty="98">Warning</MessageLevel>

        <FixCategories>Breaking</FixCategories>

        <Owner>Robert Wilczyński</Owner>

    </Rule>

</Rules>

XML, NamingRules.xml

 

 

Element         

Opis

Rules  

Zawiera wszystkie bloki opisu reguł dostępnych w bibliotece.

·         FriendlyName - Przyjazna nazwa opisywanej biblioteki reguł.

Rule    

Pojedyncza reguła.

·         TypeName - Nazwa klasy implementującej daną regułę.

·         Category     - Kategoria reguły.

·         CheckId - Unikalny identyfikator reguły. W połączeniu z atrybutami TypeName i Category tworzy unikalną grupę identyfikującą regułę. Pozwala to na wykluczenie danej reguły z procesu analizy z poziomu kodu.

Name  

Przyjazna nazwa reguły. Stosując konwencję twórców FxCop’a będzie to nazwa klasy ze spacjami.

Description     

Krótki opis podający szczegóły i potencjalne przyczyny zaistniałego problemu.

Resolution      

Szczegółowy opis postępowania, które ma na celu usunięcie zaistniałego problemu.

·         Name – nazwa służąca do określenia rozwiązań specyfikowanych, o których mowa później.

MessageLevel

Poziom istotności wiadomości skojarzonej z regułą

·         Cenrtainty - określa współczynnik pewności reguły.

FixCategories

Określa wpływ zastosowania się do zalecanego rozwiązania problemu na resztę kodu.

Owner 

Osoba odpowiedzialna za regułę (zwykle autor).

Email  

Adres email, pod którym można uzyskać wsparcie lub zgłaszać błędy dotyczące danej reguły.

URL     

Odnośnik do pełniejszej informacji o regule.

 

 

Zanim przejdziemy do pisania własnej reguły, wyjaśnię pokrótce składowe klasy BaseIntrospectionRule, po której pośrednio dziedziczy klasa implementujące regułę.

 

public virtual void BeforeAnalysis();

 

Wykonywana bezpośrednio przed analizą, może okazać się przydatna w przypadkach, gdy reguła posiada unikalny stan w trakcje wykonania, np. zapisuje w pewnej strukturze informacje potrzebne do dalszej analizy.

 

public virtual void AfterAnalysis();

 

Wykonywana jak sama nazwa wskazuje po analizie. Może być wykorzystana do wyczyszczenia ewentualnego stanu, lub zwolnienia zasobów.

 

public virtual ProblemCollection Check(Member member);

public virtual ProblemCollection Check(Module module);

public virtual ProblemCollection Check(Parameter parameter);

public virtual ProblemCollection Check(Resource resource);

public virtual ProblemCollection Check(TypeNode type);

public virtual ProblemCollection Check(string namespaceName, TypeNodeList types);

 

Przeciążona (overloaded) metoda Check odpowiada za analizę poszczególnych elementów podzespołów od dołączonych zasobów po zmienne lokalne. Wersje dostępne w klasie BaseIntrospectionRule zwracają jedynie wartość null, więc podczas pisania własnej reguły należy nadpisać (override) jedną z nich uzupełniając kodem odpowiedzialnym za analizę odpowiedniego elementu. W poprzedniej wersji FxCop’a dostępne były również wersje tej metody dla różnych członków klas jak pola czy metody. W obecnej wersji istnieje ogólna metoda przyjmująca jako argument typ Member.

 

protected Resolution GetResolution(params string[] args);

 

Pobiera szablon rozwiązania problemu zapisany w pliku XML opisującym reguły uzupełniając pola dostarczonymi parametrami.

 

protected virtual Resolution GetNamedResolution(string id, params string[] args);

 

Jedna reguła może oczywiście dotyczyć wielu elementów klasy (metody, parametry itd.) stąd każda może mieć wiele rozwiązań danego problemu. Do specyfikacji rozwiązania w zależności od elementu klasy (lub dowolnej innej przesłanki) służy atrybut Name elementu Resolution w pliku opisującym reguły.

     

public ProblemCollection Problems { get; }

 

Kolekcja, do której dodajemy w nadpisanej metodzie Check problemy wykryte przez naszą regułę.

 

W przestrzeni nazw Microsoft.Tools.FxCop.Sdk.Introspection znajduje się ponadto niezwykle przydatna klasa RuleUtilities, która oferuje statyczne metody i właściwości ułatwiające pisanie reguł. Wśród nich metoda sprawdzająca czy zmienna została dodana przez kompilator (IsCompilerGenerated), sprawdzająca czy element klasy jest konstruktorem (IsContructor) lub tablicą (IsArray). Niektóre biblioteki reguł dostarczone z FxCop’em również posiadają klasy pomocnicze zawierające specyficzne dla danej kategorii reguł metody. Po pełen wykaz dostępnych metod odsyłam do Reflector’a [5].

 

PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore

 

"You are only happy with a project, you are only

really happy with its code, when you cannot tell

which one of your guys wrote it."

Paul Sheriff

 

Paul Sheriff mawia, że naprawdę szczęśliwym z prowadzonego projektu i jego kodu można być tylko wtedy, jeśli nie wie się, który z programistów napisał dany fragment. Nie chodzi tu oczywiście jedynie o konwencje nazewnicze, ale również o pozostałe obszary pisania dobrego kodu jak projekt czy wydajność. Jako, że konwencje nazewnicze są jedną z najłatwiejszych do zaimplementowania kategorii reguł zaczniemy rozbudowę arsenału naszego policjanta właśnie od tej grupy.

 

Jako prosty przykład wybrałem regułę sprawdzającą czy prywatne pola klasy poprzedzone są prefiksem „m_”. Konwencja ta, stosowana wcześniej na przykład przez programistów C++, jest również również popularna wśród zespołów programujących w C#. Po wcześniejszych przygotowaniach zadanie wydaje sie być proste. Dodajemy konstruktor, który zainicjuje klasę bazową odpowiednią nazwą, oraz nadpisujemy jedną z przeciążonych metod Check.

 

using Microsoft.Cci;

using Microsoft.Tools.FxCop.Sdk;

using Microsoft.Tools.FxCop.Sdk.Introspection;

 

namespace NetSolutions.FxCop.Rules.Naming

{

 

            public class PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore

                                   : NamingIntrospectionRule

            {

 

                        public PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore()

                                   : base("PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore")

                        {

                                   /* */                                                   

                        }

 

 

                        public override ProblemCollection Check(Member member)

                        {

                                   Field field = member as Field;

 

                                   if (field == null || field.IsVisibleOutsideAssembly)

                                   {

                                               return null;

                                   }

                                  

                                   string fieldName = member.Name.Name;

                                   string correctIdentifierName = string.Empty;

 

                                   if ((fieldName.Length > 2) && !fieldName.StartsWith("m_"))

                                   {

                                               correctIdentifierName = "m_" + fieldName;                    

 

                                               string[] resolutionParams = new string[2]

     {fieldName, correctIdentifierName};

 

                                               Resolution resolution = this.GetNamedResolution("Field",

 resolutionParams);

                                               Problem problem = new Problem(resolution, member as Node);

                                               base.Problems.Add(problem);

                                   }

 

                                   return base.Problems;

                        }

            }

 

}

C#, PrivateFieldIdentifiersShouldBePrefixedWithMUnderscore.cs

 

Idea działania metody Check nie jest skomplikowana. Na początku sprawdzamy czy mamy do czynienia z polem, które nie jest widoczne na zewnątrz podzespołu (IsVisibleOutsideAssembly), gdyż tylko do takich pól chcemy ograniczyć naszą regułę. Pomimo iż w przedstawionym fragmencie kodu rzutujemy do spodziewanego typu, w celu sprawdzenia przeznaczenia członka klasy (pole, metoda itd.) można również użyć również właściwości Member.NodeType. Następnie pobieramy nazwę identyfikatora pola member.Name.Name (member.Name zwraca obiekt klasy Identifier, który z kolei posiada właściwość Name – stąd nazwa identyfikatora pola, a nie po prostu nazwa pola). W przypadku stwierdzenia niezgodności nazwy z konwencjami nazewniczymi egzekwowanymi przez regułę dodajemy napotkany problem do kolekcji problemów. Ta część jest charakterystyczna dla wszystkich reguł, zasługuje więc na nieco dokładniejszy opis.

 

Konstruktor klasy Problem, którego użyliśmy do utworzenia nowego problemu przyjmuje dwa parametry: pierwszy typu Resolution (czyli sugerowane rozwiązanie problemu pobrane z pliku XML opisującego regułę), drugi zaś typu Node. Podczas kompilacji podzespołu w trybie Debug, linker tworzy dodatkowy plik z rozszerzeniem PDB (Program Database), który zawiera między innymi informacje kojarzące niektóre instrukcje programu z odpowiadającymi im wierszami kodu. Informacje te są wykorzystywane są przez FxCop’a do wskazania wiersza, w którym znajduje się zidentyfikowany błąd. Wiersz ten (jeśli tylko został zachowany w pliku PDB) będzie odpowiadał węzłowi (Node) podanemu jako drugi parametr konstruktora.

 

Wspominałem już wcześniej o szablonie rozwiązania problemu i specyfikowanych rozwiązaniach. Szablon pozwala na umieszczenie dodatkowych informacji specyficznych dla danego wystąpienia problemu w instrukcji rozwiązania (resolution). Są one przekazywane do metody GetResolution (lub GetNamedResolution) w postaci tablicy stringów i w naszym przypadku służą do uzupełnienia szablonu niepoprawną (z punktu widzenia przyjętych konwencji) nazwą identyfikatora pola oraz nazwą sugerowaną.

 

<Resolution Name="Field">Zmień nazwę prywatnego pola tak, aby odpowiadała przyjętym standardom. Nazwa {0} powinna zostać zastąpiona nazwą {1}.</Resolution>

 

W projekcie dołączonym do artykułu znajduje się podobna reguła sprawdzająca czy nazwy zmiennych lokalnych rozpoczynają się od litery „l”.

 

MethodsShouldReturnEmptyStringsInsteadOfNulls

 

Kolejna reguła, której implementacji się podejmiemy, sprawdza czy metoda zwracająca typ string nie zwraca wartości null. Zgodnie z zaleceniami zawartymi w dokumencie Design Guidelines for Class Library Developers metody te powinny zwracać pusty łańcuch (String.Empty).

 

W przypadku tej reguły niezbędna okazuje się podstawowa wiedza o języku pośrednim (IL), do którego kompilowane są podzespoły. Przed przystąpieniem do opracowania odpowiedniego algorytmu zapoznajmy się ze zoptymalizowanym przez kompilator kodem (tryb Release) odpowiedzialnym za instrukcję return null.

 

.method public hidebysig instance string BadGetString() cil managed

{

      .maxstack 1

      L_0000: ldnull

      L_0001: ret

}

MSIL, TestClass.cs

Instrukcja ldnull wkłada na stos ewaluacyjny naszej metody wartość null, zaś ret przerzuca pierwszy element tego stosu na stos ewaluacyjny metody wywołującej. Każde wystąpienie takich dwóch instrukcji powinno być więc identyfikowane jako błąd.

 

Jak już wspominałem mechanizm refleksji nie oferuje wygodnego dostępu do instrukcji wykonywanych przez metody. Co prawda jest to możliwe, ale jednocześnie na tyle pracochłonne, że twórcy FxCop’a postanowili wzbogacić silnik introspekcji o mechanizm, który wykonuje tą i wiele innych czynności za nas. W obecnej wersji FxCop’a mamy do dyspozycji kolekcję Instructions klasy Method uzupełnioną wszystkimi instrukcjami wywoływanymi w metodzie. Każda instrukcja posiada z kolei kod operacji OpCode, oraz „wartość” Value (parametr instrukcji). Poniżej uproszczona wersja reguły wychwytująca przedstawiony problem.

 

 

public override ProblemCollection Check(Member member)

{

            Method method = member as Method;

                                  

            if (method == null)

            {

                        return null;

            }

            if (method.ReturnType == null || method.ReturnType.TypeCode != TypeCode.String)

            {

                        return null;

            }

                                  

            string methodName = method.Name.Name;

            for (int retIndex = 0; retIndex < (method.Instructions).Length; retIndex++)

            {

                        Instruction ret = method.Instructions[retIndex];

                        if (ret.OpCode == OpCode.Ret)

                        {

                                   if (retIndex > 0)

                                   {

                                               Instruction ldnull = method.Instructions[retIndex - 1];

                                               if (ldnull.OpCode == OpCode.Ldnull)

                                               {

                                                           Resolution resolution = this.GetResolution();

                                                           Problem problem = new Problem(resolution, node);

                                                           base.Problems.Add(problem);

                                               }

                                   }

                        }                                                        

            }

            return base.Problems;

}

C#, MethodsShouldReturnEmptyStringsInsteadOfNulls.cs

 

Tym razem sprawdzamy czy mamy do czynienia z metodą, która zwraca wartość typu String. Następnie przeglądamy kolekcję instrukcji w poszukiwaniu instrukcji powrotu (ret) i sprawdzamy czy poprzednia instrukcja nie wkłada na stos ewaluacyjny wartości null (instrukcja ldnull).

 

Sytuacja komplikuje się jeśli reguła ma działać również dla bibliotek skompilowanych bez optymalizacji (w trybie Debug). Metoda posiada wtedy dodatkową zmienną lokalną. Stloc.0 przypisuje zmiennej lokalnej o indeksie zero pierwszą wartość ze stosu ewaluacyjnego czyli włożona na stos instrukcją ldnull wartość null. Wartość zmiennej wraca ponownie na stos za sprawą instrukcji ldloc.0 i dopiero wtedy przerzucana jest na stos ewaluacyjny metody wywołującej instrukcją ret.

 

.method public hidebysig instance string BadGetString() cil managed

{

      .maxstack 1

      .locals (

            string text1)

      L_0000: ldnull

      L_0001: stloc.0

      L_0002: br.s L_0004

      L_0004: ldloc.0

      L_0005: ret

}

MSIL, TestClass.cs

 

Dołączona do artykułu biblioteka reguł zawiera pełniejszą implementację, która uwzględnia również taki scenariusz.

 

Sformułowania „pełniejsza” nie jest przypadkowe bowiem napisanie reguły, która byłaby w stanie wychwycić każde wystąpienie opisanego problemu nie jest zadaniem łatwym. Po pierwsze ze względu na naturę analizy statycznej nie zawsze jesteśmy w stanie stwierdzić jakie wartości będą posiadać zwracane zmienne. Poza tym zwracana wartość nie musi pochodzić ze zmiennych lokalnych. Mogą to być pola, metody lub parametry metod, których wartości nie da się przewidzieć nawet poprzez analizę grafów wywołań. Zaprezentowany przykład ma na celu jedynie pokazanie stopnia szczegółowości z jakim możemy analizować biblioteki, nie jest zaś próbą implementacji w pełni działającej reguły. Ten przypadek pokazuje nam również ograniczenia FxCop’a oraz samej statycznej analizy kodu i prawdopodobnie dlatego nie doczekał się implementacji przez twórców reguł dostarczonych z narzędziem.

 

Debugowanie reguł

 

Bardziej skomplikowane reguły (a wśród nich te, które wywołują metody Chceck rekurencyjnie) są ,jak każdy fragment kodu, narażone na błędy programisty. W niektórych przypadkach konieczne więc okaże się debugowanie reguł. Najbardziej efektywnym podejściem jest tutaj dobrze wszystkim znane podłączenie debuggera z Visual Studio do procesu w którym będzie wykonywał się debugowany kod. W naszym przypadku będzie to proces FxCop’a. Po załadowaniu biblioteki reguł, którą mamy zamiar debugować i ewentualnym wyodrębnieniu interesującej nas reguły wystarczy rozpocząć proces analizy. FxCop zatrzyma się wtedy na ustalonych przez nas w kodzie punktach wstrzymania (breakpoint) co pozwoli na wychwycenie ewentualnych błędów w kodzie reguły.

FxCop posiada również wbudowane wsparcie dla debugowania reguł przy użyciu mechanizmu śledzenia (trace), dostępnego przez przestrzeń nazw System.Diagnostic. Jako że sposób ten wymaga wprowadzania zmian w plikach konfiguracyjnych FxCop’a oraz w samych bibliotekach reguł, pomimo jego niewątpliwej przydatności, pominę szczegóły i odeślę zainteresowanych do SDK jednej z poprzednich wersji programu [7].

 

 

Przyszłość FxCop’a

 

FxCop okazał się narzędziem na tyle popularnym (i potrzebnym), że postanowiono dołączyć go do Visual Studio 2005 w wersji Team System. Zapowiadana integracja z IDE, oraz automatyczna analiza po skompilowaniu kodu z pewnością sprawą, że więcej zespołów będzie decydowało się na używanie FxCop’a. Dobrą wiadomością dla tych, którzy z oczywistych względów nie będą mogli skorzystać z wersji Team System jest to, że FxCop będzie nadal dostępny jako darmowe, samodzielne narzędzie.

 

Podsumowanie

 

Pomimo iż FxCop jest nadal we wczesnej fazie rozwoju i ustępuje nieco komercyjnym produktom (np. Compuware DevPartner) pod względem możliwości oraz ilości dostarczonych reguł jest to bez wątpienia odpowiedni wybór dla mniejszych warsztatów programistycznych, które nie mogą pozwolić sobie na inwestycję w drogie, komercyjne rozwiązania. W prawdzie nadal niekompletne SDK sprawia, że trudno jest oprzeć o FxCop’a proces kontroli jakości kodu, jest to jednak narzędzie, które początkującym adeptom programowania opatego o platformę .Net dostarcza wielu cennych wskazówek i pozwala na pisanie lepszego kodu nawet tym mniej doświadczonym. Mam nadzieję, że przedstawione przykłady zachęcą do używania tego narzędzia i rozbudowy jego możliwości o własne reguły.

 

Dodatkowe zasoby

 

[1] Design Guidelines for Class Library Developers

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconnetframeworkdesignguidelines.asp

 

[2] Designing .NET Class Libraries – Seria Webcastów o projektowaniu bibliotek:

http://msdn.microsoft.com/netframework/programming/classlibraries/default.aspx

 

[3] Designing .NET Class Libraries: FxCop In Depth – Webcast poświęcony FxCop’owi:

http://msdn.microsoft.com/netframework/programming/classlibraries/fxcop/

 

[4] Spis kodów operacji języka MSIL z wytłumaczeniem

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemreflectionemitopcodesmemberstopic.asp

 

[5] Lutz Roeder’s Reflector – program skutecznie rekompensujący tymczasowy brak SDK:

http://www.aisto.com/roeder/dotnet/

 

[6] FxCop 1.312 – Wersja użyta przy pisaniu artykułu

http://www.gotdotnet.com/team/fxcop/FxCopInstall1.312.MSI

 

[7] Using the FxCop 1.2 Beta SDK

http://www.gotdotnet.com/team/fxcop/docs/gotdotnetstyle.aspx?url=UsingFxCopSdk.htm

Załączniki:

Komentarze 0

pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.