Статический анализ кода в Java: что под капотом

Оказалось, статический анализ кода стал модной темой, такой, что даже украинские сеньоры от сохи начали интересоваться: какие кнопки там надо нажимать ;)

Так уж получилось, что тема анализа исходного кода стала одним из моих первых увлечений в мире разработки ПО: еще в прошлом веке, работая в академии наук, я занимался системой статического анализа кода, которая стала первым реальным приложением termware, и позже выросла в JavaChecker (redmine.gradsoft.ua/projects/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 выглядит следующим образом:

*PLACEHOLDERS_PRE_2*

(То есть мы проверяем, что сравниваться между собой могут только представители примитивных типов и перечислений, либо это должно быть сравнение с null).

Написать подобные правила с разной долей изящества можно практически для любого из современных средств анализа кода (с учетом ограничений представления, о котором было написанно в начале статьи), также все современные средства поставляются в виде API, позволяющим их интегрировать со средствами сборки (типа ant или maven) или оболчкой запуска (типа Sonar или xradar). Если можно говорить о «конкурентности» (коммерчески этот сектор не очень привлекателен из-за обилия систем с открытым кодом) то каждая из систем статического анализа имеет свой набор ‘killer feature’:

  • PMD — проверку на совпадающие участки кода (то ест определение copy & paste участков)
  • FindBug — прогонка возможности обращения к null объекту.
  • JavaChecker — встраиваемость, высокоуровневый язык правил и возможность работы с синаксисом и семантикой «в одном флаконе»

А «полем битвы» на переднем фронте компьютерной науки является баланс между производительностью и перебором — простые задачи решаются просто, сложные задачи как правило сводятся к переборной проблеме с экспоненциальной сложностью вычислений, и вот как сделать так, что бы некоторые из этих сложных проблем решались за приемлемое время для реальных программных комплексов и составляет предмет исследований.

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

  • легкие проблемы (типа соблюдения стандартов кодирования или поиска детских ошибок типа сравнения строк с помощью оператора равенства а не equals)

  • сложные проблемы (типа отслеживания состояния гонок или зацикливания)

Так вот:


  • автоматизация легких проблемы не нужна (точнее: если вам приходится прибегать к специальному средству автоматизации для обеспечения стандартов кодирования, то скорее всего у вас серьезные проблемы с текучкой кадров и командой)
  • автоматизация сложных проблем
    • нужна не всегда. Отслеживание корректности параллельного распределения ресурсов будет иметь бизнес-смысл для операционной системы, но не для приложения для представления данных из БД в виде Web форм.
    • в принципе, промышленно еще не работает. Решить такие задачи ‘попутно’, просто добавляя еще один шаг к сборке невозможно.

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

Вторая проблема — невозможность нетривиального использования без привязки исходного кода к какому-то выбранному средству. К примеру, посмотрим на нашу проверку правильности сравнения, и обратим на то, что на самом то деле сравнить два объекта на равенство все-таки иногда нужно и это оправданно. (Скажем в собственной реализации хеш-таблицы) Естественно в каком-то конкретном месте можно отключить проверку с помощью специальной аннотации или управляющего комментария (зачем нужны управляющие комментарии — что бы не заставлять клиентов таскать с собой нашу библиотеку в зависимостях). Получается вам надо в исходной коде проверяющей программы писать что-то для конкретного средства анализа кода, при чем там тоже можно сделать ошибки... кажется, мы получили усложнение сопровождения вместо облегчения.

В этом контексте будет уместно рассказать про JSR305. Как я уже говорил, ‘killer feature’ FindBug является анализ потока исполнения на возможность обращения к null объекту.
Пример:
*PLACEHOLDERS_PRE_3*
Мы знаем, что контракт интерфейса 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 — cейчас неважно) потом писать маппинг между этими DTO и hibernate POJO, что вело бы к довольно большим объемам работ. Что сделали мы — непосредственно отобразили существующий java объекты в PHP: написали генератор, который анализирует POJO объекты и генерирует код для определений cоответвующих PHP объектов и их сериализации в JSON: redmine.gradsoft.ua/...​ungleplatform/wiki/Phpjao

Ну и собственно когда имеет смысл использовать средства подобные javachecker — когда у вас большой массив кода и надо сделать что-то конкретное.

Конечно, заметки такого объема крайне недостаточно для описания области, но надеюсь, какие-то начальные сведения и ’вкус’ области при желании восстановить можно. Удачной работы и не бойтесь заглядывать под капот ;)

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

👍НравитсяПонравилось0
В избранноеВ избранном0
Подписаться на автора
LinkedIn



Підписуйтесь: Soundcloud | Google Podcast | YouTube


18 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Что есть «cat & copy»?

cut & copy. Спасибо, исправляю

У меня есть сильное подозрение, что то, что вы хотите сказать, называется «copy & paste» или в простонаречии копипаста.

Ну это уже более или менее одно и то-же. сopy & paste, конечно, точнее — заменю ;)

Что сделали мы — непосредственно отобразили существующий java объекты в PHP: написали генератор, который анализирует POJO объекты и генерирует код для определений cоответвующих PHP объектов и их сериализации в JSON:

Если маппинг хибернэйта сделан в ксмл файле а не анотациях, то было бы намного изящнее написать протобуферы которые сигнатурой соответствуют pojo, и юзать их вместо pojo. Кстати сгенерировать протобуферры можно было бы и вашими тулзами.

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

Объем кода. (Там все преобразование занимает 300 строк). Но вобще — да, в принципе можно было бы и reflection обойтись.

Не думаю что сильная экономия получится, т.к. разница будет только в коде который получает список полей их типов и методов из класса.

Там же еще наследование (при этом надо знать что игнорировать и что перевести) и константы и енумы. Я так ’на глазок’ оцениваю разницу в трудоемкости раза в два/три (если отбросить входной барьер изучения termware).

Ну тебе конечно виднее, но никаких затыков в рефлекшне по сравнению с вашей тулзой в описанных случаях я лично не вижу.

Затыков нет, просто трудоемкость и легкость модификации (ну типа зачем писать на низкоуровневом языке, когда можно на высокоуровневом)

ИМХО, для Java-ы статический анализ (при сборке и/или просто отдельный инструмент) «не нужен» — от него мало пользы, уж очень хорошо для этого подходит сам язык и ИДЕ сейчас покрывают практически все проблемы типа «опечатка» (== вместо equals, например).

Самое место для таких «тулов» в языках с динамической типизацией (утиной типизацией), например, в javascript. Часто даже ’strict mode’ (который «работает» во время выполнения) и JsDoc не помогает, а при сборке какой-то из линтов + компилятор с высоким уровнем логов может очень упростить разработку.

Ну тут зависит от задач. То есть можно просто прикрутить к сборке анализатор, без какой-либо цели, тогда мало пользы. Но бывают еще задачи в стиле: вот нам хочеться это API сделать по другому, давайте оценим объем изменений кодовой базы, или — ой, такой-то объект может быть частично инициализирован, давайте посмотрим всюду ли мы вызываем init. В таких случаях без статического анализатора — никуда

Динамические языки — да, перспективное направление. (мы, кстати, работаем с анализом PHP, но показывать что=то еще рано)

мы, кстати, работаем с анализом PHP, но показывать что=то еще рано

Так а вот у фейсбука уже нечто рабочее есть — github.com/facebook/pfff

Оно в основном пхп, к сожалению, но как минимум уже что-то есть. Или хочется другого?

Ну для php есть штук 5 разных тулзовин: stackoverflow.com/...t-for-php-files

Другое у нас то как и в javachecker — высокоуровнеівые правила и можно будет делать прогон абстрактной интерпритации. Ну и сложно сказать, доведем ли мы это дело до внешнего продукта или нет, потому как непонятно с монетизацией.

Простите, я никогда не сталкивался с понятием «Статистический анализ кода», не могли бы вы дать ссылки «на почитать».

Или это просто опечатка? и имелось ввиду «статический анализ кода» (по тексту далее встречается именно этот термин да и по сути статьи похоже)?

ой... это в заголовке такое — надо же (исправляю)

Чует мое сердце — комментариев тут будет не много :)

Подписаться на комментарии