Статичний аналіз коду в java: що під капотом

Виявилося, статичний аналіз коду став модною темою, такий, що навіть українські сеньйори від сохи почали цікавитися: які кнопки там треба натискати;)

Так вже вийшло, що тема аналізу вихідного коду стала одним з моїх перших захоплень у світі розробки ПЗ: ще в минулому столітті, працюючи в академії наук, я займався системою статичного аналізу коду, яка стала першим реальним додатком termware, і пізніше виросла в JavaChecker (redmine.gradsoft.ua/... cts/javachecker ).

Тому в цій колонці я дам невеликий огляд «сутнісних проблем» статичного аналізу коду Java зсередини, мовою трохи більше технічним, ніж про податки, але все ще «на пальцях» (не знаю, чи буде зрозуміло зовсім вже анонімним розробникам, але базової вищої освіти має бути достатньо).

Отже, що ми згадуємо при слові аналіз коду - в першу чергу lint зі світу C, в другу - підхід інваріантів Дейкстри і контрактне програмування; в світі Java - невдалий ключове слово assert, запис з найбільшою кількістю голосів у Sun Java Bug Database і неактивний JSR 305, як невдача лобового застосування. З удач - рефакторинг в сучасних IDE, збір метрик і перетворення коду. Ну і нарешті, деякі з java розробників бачать лише верхівку айсберга у вигляді додаткового генератора звітів, який можна прикрутити до процесу складання та отримати додаткову інформацію про порушення стилю кодування і можливі помилки.

Більш систематично - аналіз коду це можливість програми прочитати код аналізованої програми в будь-якій формі, «зрозуміти» його і видати якусь інформацію. Відповідно, практично всі аналізатори коду можна уявити собі як пошук в певному поданні програми (можливо з перетвореннями) певних патернів і подальший докладний аналіз знайдених ділянок.

Можна ввести «типологію» коштів аналізу коду на основі того, яке уявлення коду вони аналізують і наскільки глибоко вони його розуміють. Наприклад, якщо взяти дві найбільш поширені системи (PMD і Findbugs), то PMD працює з поданням програми у вигляді AST дерева, FindBugs - у вигляді байт-коду. Наш JavaChecker працює з поданням програми у вигляді «семантичного дерева», де інформація з AST доповнена доступом до семантичного контексту. Відповідно, скажімо, написати перевірку угод про стандарт кодування на JavaChecker і PMD - тривіально, на FindBugs - неможливо. І навпаки - перевірку можливості звернення до нульової посиланням на PMD написати неможливо, на JavaChecker або FindBugs - досить просто.

Самі визначники 'ризикованих' місць пишуться, як правило, на вбудованих мовами або Java, наприклад тест для визначення 'небезпечного присвоювання' для JavaChecker виглядає наступним чином:

Checkers (
[
 define (ObjectComparisonWithEquality, expressions,
          "Comparing objects using equiality operator",
        MODEL_RULESET,
        ruleset (
         import (general, apply),
         import (List, car),
         import (List, cdr),
         EqualityExpressionModel ($ x, $ y, $ op, $ ctx)
             [
                $ X! = NullLiteral ()
                & &
                $ Y! = NullLiteral ()
                & &
               ! Car ($ ctx.getExpressionModel (). GetSubExpressions ())
                        . GetType (). IsPrimitiveType ()
                & &
               ! Car ($ ctx.getExpressionModel (). GetSubExpressions ())
                        . GetType (). IsEnum ()
                & &
               ! Car (cdr ($ ctx.getExpressionModel (). GetSubExpressions ()))
                        . GetType (). IsPrimitiveType ()
                & &
               ! Car ($ ctx.getExpressionModel (). GetSubExpressions ())
                        . GetType (). IsEnum ()
             ]
              ->True
                 [ViolationDiscovered (ObjectComparisonWithEquality,
                              "Object compared by == or! =", S ($ x, $ y))]
             ! ->False
        ), True)
]
);

(Тобто ми перевіряємо, що порівнюватися між собою можуть тільки представники примітивних типів і перерахувань, або це має бути порівняння з null).

Написати подібні правила з різною часткою витонченості можна практично для будь-якого з сучасних засобів аналізу коду (з урахуванням обмежень уявлення, про який було написано на початку статті), також всі сучасні засоби поставляються у вигляді API, що дозволяє їх інтегрувати з засобами складання ( типу ant або maven) або оболчке запуску (типу Sonar або xradar). Якщо можна говорити про «конкурентності» (комерційно цей сектор не дуже привабливий через велику кількість систем з відкритим кодом) то кожна з систем статичного аналізу має свій набір 'killer feature':

А «полем битви» на передньому фронті комп'ютерної науки є баланс між продуктивністю і перебором - прості завдання вирішуються просто, складні завдання як правило зводяться до переборний проблему з експоненційної складністю обчислень, і от як зробити так, що б деякі з цих складних проблем вирішувалися за прийнятний час для реальних програмних комплексів і складає предмет досліджень.

Так що для нашого анонімного «розробника Джо», великої різниці між усіма сучасними засобами статичного аналізу дійсно немає: всі вони надають базовий набір перевірок і всі вони інтегруються в процес розробки протягом декількох годин.

Тільки от лихо, користь від такого вбудовування задоволена обмежена: грубо кажучи, є 2 типи проблем, які вирішують кошти статичного аналізу:

Так от:

означає це що статичний аналіз потрібний у реальному житті - зовсім ні. Якщо у вас є велика база інфраструктурного коду, який постійно переробляється, то навіть таке обмежене застосування засобів аналізу виправдано.

Друга проблема - неможливість нетривіального використання без прив'язки вихідного коду до якогось обраному засобу. Приміром, подивимося на нашу перевірку правильності порівняння, і звернемо на те, що на самому те справі порівняти два об'єкти на рівність все-таки іноді потрібно і це виправдано. (Скажімо у власній реалізації хеш-таблиці) Природно в якомусь конкретному місці можна відключити перевірку за допомогою спеціальної анотації або керуючого коментаря (навіщо потрібні керуючі коментарі - що б не змушувати клієнтів тягати з собою нашу бібліотеку в залежностях). Виходить вам треба у вихідній коді перевіряє програми писати щось для конкретного засобу аналізу коду, при чому там теж можна зробити помилки ... здається, ми отримали ускладнення супроводу замість полегшення.

У цьому контексті буде доречно розповісти про JSR305. Як я вже говорив, 'killer feature' FindBug є аналіз потоку виконання на можливість звернення до null об'єкту.

Приклад:

void myMethod (Map map) {
       ...
       MyClass x = map.get (key);
       x.myMethod (y)
       ...
   }

Ми знаємо, що контракт інтерфейсу Map увазі, що при метод get може повернути null, якщо об'єкт з ключем key не знаходиться в колекції. Cоответственно в наступному рядку, виклику myMethod можливий збій через звернення до null. У мовах типу scala або haskell ми використовуємо щось на кшталт Option [T] для неможливості подібних помилок, а ось в Java потрібен статичний аналіз, який грунтується на тому, що ми всі методи, які можуть повернути null позначаємо анотацією @ Nullable і потім перевіряємо, що б до полів або методів об'єктів, що повертаються такими функціями, не було доступу без попереднього перевірки на null. JSR 305 передбачала ввести в мову Java набір стандартних анотацій для управління процесом статичного аналізу і забезпечення маркування методів для алгоритмів аналізу потоку даних (тобто @ Nullable і ще щось в стилі @ Safe для запобігання sql-иньекций). Автор FindBug свого часу ініціював JSR-305, але потім з невідомих причин активність у цьому напрямку зійшла нанівець і зараз це розширення позначено в БД JSR як неактивний. Інша пропозиція по стандартизації - JSR 308, визначає більш загальний фреймворк (types.cs.washington.edu/jsr308 /) який даcт можливість розробникам статичних аналізаторів використовувати інформацію про програму, зібрану компілятором.

У java співтоваристві ходять чутки, що можливо, деякі стандартні анотації, спочатку запропоновані в JSR-305 і JSR-308, будуть включені в JDK7, а відповідні перевірки будуть працювати в базовому компіляторі. Наскільки вони відповідають дійсності - сказати складно.

Чи обмежується використання статичного аналізу генерацією звітів про потенційні проблеми --- немає. Ще одна, мало не найбільша область застосування - це нутрощі IDE і засобів розробки. Також аналіз коду може бути корисний при інтеграції існуючої кодової бази зі сторонніми системами. Так як javachecker надає відкритий фреймворк, для вбудовування в інші системи, у нас є випадки і такого використання:

Наприклад, необхідно було пов'язати внутрішню систему, побудовану на стеку Spring/Hibernate c фронтенд на PHP. Відповідно, потрібно було знайти спосіб представлення Java Cущность (POJO) в PHP і реалізувати проміжний шар, який здійснює конвертацію. Без використання засобів аналізу коду, потрібно було б або з боку PHP працювати з неструктурованими об'єктами, або описувати структуру DTO (data transfer objects) об'єктів на якомусь IDL (google protocol buffers, zeroc, thrift - зараз неважливо) потім писати маппінг між цими DTO і hibernate POJO, що вело б до досить великих обсягів робіт. Що зробили ми - безпосередньо відобразили існуючий java об'єкти в PHP: написали генератор, який аналізує POJO об'єкти і генерує код для визначень cоответвующіх PHP об'єктів і їх серіалізациі в JSON: redmine.gradsoft.ua/... orm/wiki/Phpjao

Ну і власне коли має сенс використовувати кошти подібні javachecker - коли у вас великий масив коду і треба зробити щось конкретне.

Звичайно, замітки такого обсягу вкрай недостатньо для опису області, але сподіваюся, якісь початкові відомості і 'смак' області при бажанні відновити можна. Вдалої роботи і не бійтесь заглядати під капот;)

Опубліковано: 15/07/11 @ 03:39
Розділ Різне

Рекомендуємо:

Успішні кейси просування в Яндексі. Частина 3. Псевдо-основне меню.
Дайджест тижня, 15 липня
Чи впливають програмісти на бізнес компанії?
ДОУ Хакатон у Львові - звіт
Junior-Middle-Senior - якими папугами міряємо?