Статический анализ кода в 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 — когда у вас большой массив кода и надо сделать что-то конкретное.
Конечно, заметки такого объема крайне недостаточно для описания области, но надеюсь, какие-то начальные сведения и ’вкус’ области при желании восстановить можно. Удачной работы и не бойтесь заглядывать под капот ;)
Все про українське ІТ в телеграмі — підписуйтеся на канал DOU
18 коментарів
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.