Непрерывный статический анализ кода

Иван Пономарёв, КУРС/МФТИ

vessel

Что такое статанализ?

  • Wikipedia: «Анализ программного обеспечения, производимый без реального выполнения исследуемых программ».

  • Здравый смысл: Любая проверка исходного кода, требующая только исходный код (без тестов).

Чего в принципе не может СА?

Чего в принципе не может СА?

Зависнет или остановится?

def halts(f):
    # false, если программа зависает
    # true, если завершается за конечное время
def g():
    if halts(g):
        while(True):
            pass

Теорема Райса

Вычисляет ли функция квадрат числа?

def is_a_squaring_function(f):
    # true, если функция вычисляет квадрат
    # false, если не вычисляет
def halts(f):
    def t(n):
        f()
        return n * n
    return is_a_squaring_function(t)

Статанализ не найдёт то, что решается на уровне языка

  • object has no attribute (если динамическая типизация)

  • NPE (если нет Null Safety)

swamp

Близкое динамическое болото

pure lake

Труднодоступное статическое озеро

А что же статанализ?

no unicorn
horse

Разнообразие средств статанализа

f1
  • Проверка стиля кодирования (checkstyle, flake8)

  • Поиск характерных ошибок в коде (spotbugs, IDEA, PVS-Studio)

  • Проверка валидности ресурсных файлов (xmllint, YAMLlint, JSONLint)

Разнообразие средств статанализа

f2
  • Компиляция/парсинг (ansible --syntax-check, terraform validate)

  • Предупреждения компиляторов

  • Проверка правописания

  • Конфигурационные тесты

Больше анализаторов!

  • Google <Your Language> static analyzer

  • Google <Your Language> linter

Кто программирует на Bash?

shellcheck cap
shellcheck

Intellij IDEA в CI

  • bin/format.sh — форматирование кода

  • bin/inspect.sh — инспекции (с выводом в .xml)

idea jenkins

Что из этого использовать?

Всё!

Как это всё использовать?

  • Однократное применение анализа бессмысленно

  • Анализ должен производиться непрерывно и автоматически

  • Результаты анализа должны определять quality gates

Роль и место СА в конвейере поставки

cd

Jez Humble, David Farley. Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation. Addison-Wesley, 2011

Типовой конвейер сборки

 
 

pipeline2

Типовой конвейер сборки

«Фильтрующая способность»
 

raising2
pipeline2

Типовой конвейер сборки

Сложность, стоимость,
время работы, вероятность сбоя

raising2
pipeline2

Многоступенчатый фильтр

 

water filtration

Многоступенчатый фильтр

Размер пропускаемого загрязнения
Пропускная способность

decreasing
water filtration

Многоступенчатый фильтр

Сложность, стоимость
 

raising
water filtration

Вывод

  • Статанализ — «фильтр грубой очистки» в начале каскада фильтров

  • В отдельности от других — не работает

  • Другие без него работают хуже

Случай из практики: долгий отклик

resource.json

{
  "key": "value with "unescaped quotes" "
}
  • Все UI тесты падают.

  • Но это происходит спустя дни.

Случай из практики: лечение

  • Добавляем JSONLint в начало пути

find . -name \\*.json -print0 | xargs -0 -n1 -t jsonlint -q
  • Отклик на проблему идёт сразу

  • PROFIT

Внедрение в legacy-проект

Внедрение в legacy-проект

Знакомая картина?

legacy

Оставить нельзя пофиксить!

Пофиксить автоматически?

Google + Stackoverflow:

  • 'sed remove trailing spaces'

 find . -name '*.py' -print0 | xargs -0 -n1 -t \
    sed -i -r 's/\s+$//'
  • 'bash add a newline to the end of a file'

find . -name '*.java' -print0 | xargs -0 -L1 bash \
    -c 'test "$(tail -c 1 "$0")" && printf "\r\n" >> $0'
  • etc etc

Автофикс

  • Javascript: eslint --fix

Spotless: идемпотентный автоформаттер

Spotless can format

<java | kotlin | scala | sql | groovy | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything>

using

<gradle | maven | anything>

Quality Gates

porta del paradiso

Пороговое значение находок

  • «Если меньше 100 находок, то код ОК»

  • ДАНО: в коде 90 находок и код ОК.

  • Добавляем Null Pointer Dereference.

  • У нас 91 находка, код всё ещё ОК?

Вывод: не используйте данный метод!

Suppression Profile

  • Старые находки — в игнор

  • Новые находки — не пропускаем

Suppression Profile

Наивный подход:

<file name="AppProperties.java">
  <error line="31" column="5" message="Missing a Javadoc comment."/>
  <error line="36" column="5" message="Missing a Javadoc comment."/>
</file>

Добавляем текст в начало файла…​

…​номера строк "уползли" и все находки снова появились.

Suppression Profile

Подход PVS-Studio --- хеши строк:

{
  "FileName": "CelestaParser.java",
  "ErrorCode": "V6021",
  "CodePrev": -1464702071,
  "CodeCurrent": -1679070819,
  "CodeNext": 35764079
}

Suppression Profile

Вывод: метод хорош, но труднодоступен

Проверка правописания

hard to spell

Проверка правописания

aspell

Проверка документации:

for f in $(find . -name '*.adoc'); do \
   cat $f | aspell --master=ru \
   --personal=./dict list; done \
   | sort | uniq

Проверка литералов и комментариев:

for f in $(find . -name '*.java'); do \
   cat $f | aspell --mode=ccpp \
   --master=ru --personal=./dict list; done\
   | sort | uniq

Проверка правописания

  • Храните пользовательский словарь в проекте

  • Quality Gate: не должно быть незнакомых спелчекеру слов.

Упавшая проверка

aspell fail

Проверка правописания

Вывод: spellchecker может быть частью пайплайна

Храповик

ratchet

Принцип работы

ratchet p0.png

Принцип работы

ratchet p1.png

Упавшая проверка

jenkins fail2

Принцип работы

ratchet p2.png

Принцип работы

ratchet p3.png

Много модулей/инструментов

Вид метаданных:

         # warnings.yml
         celesta-sql:
           checkstyle: 434
           spotbugs: 45
         celesta-core:
           checkstyle: 206
           spotbugs: 13
         celesta-maven-plugin:
           checkstyle: 19
           spotbugs: 0
         celesta-unit:
           checkstyle: 0
           spotbugs: 0

Упавшая проверка

ratchet report

Как это реализовано у нас

  • Jenkins scripted pipeline

  • Jenkins shared libraries in Groovy

  • JFrog Artifactory для хранения метаданных о сборках

Парсинг XML-вывода анализаторов

<checkstyle>
  <file name="...">
    <error line="1" severity="..." message="..."/>
  </file>
    ...
</checkstyle>
private Map countModule(prefix) {
    def count = [:]
    def f = new File("${prefix}/target/checkstyle-result.xml")
    if (f.exists()) {
        def checkstyle = new XmlSlurper().parseText(f.text)
        count.put("checkstyle", checkstyle.file.error.size())
    }
    . . .
    count
}

Скачиваем данные о последней сборке

def server = Artifactory.server 'ART'
def downloadSpec = """
        {"files": [
            {
                "pattern": "warn/${project}/*/warnings.yml",
                "build": "${project} :: dev/LATEST",
                "target": "previous.yml",
                "flat": "true"
            }
            ]
        }"""
server.download spec: downloadSpec
oldWarnings = readYaml file: 'previous.yml'

Шаг храповика

stage ('Ratcheting') {
    def warningsMap = countWarnings()
    writeYaml file: 'target/warnings.yml', data: warningsMap
    compareWarningMaps oldWarnings, warningsMap
}

Jenkins Warnings NG Plugin

Собирает и читает отчёты всех известных анализаторов

def checkstyle
  = scanForIssues tool: checkStyle(pattern: '**/cs.xml')

def spotbugs
  = scanForIssues tool: spotBugs(pattern: '**/spotbugs.xml')

def idea
  = scanForIssues tool: ideaInspection(pattern: 'target/idea_inspections/*.xml')

def eslint
  = scanForIssues tool: esLint(pattern: '**/eslint.xml')

publishIssues issues: [checkstyle, spotbugs, idea, eslint]
. . .

Jenkins Warnings NG Plugin

Красиво отображает

warnings ng

Jenkins Warnings NG Plugin

Можно программировать Quality Gates, в т. ч. в виде разницы с reference build:

recordIssues tool: java(pattern: '*.log'),
             qualityGates: [[threshold: 1,
                             type: 'TOTAL',
                             unstable: true]]

Храповик: ожидание

expected ratchet

Храповик: реальность

actual ratchet

Случай из практики

Кто здесь видит проблему?

#.travis.yml
. . .
install:
  - pip install yamllint
  - pip install ansible-lint

script:
  . . .
  # Check YAML validity
  - yamllint -c yamllint.yml .

  # Ansible code static analysis
  - ansible-lint . . .
  - ansible-lint . . .
  - ansible-lint . . .

Невоспроизводимая сборка

unstable update.png

При замене фильтров бывает грязно!

dirty filter

Фиксируем версии всего!

#.travis.yml
. . .
install:
  - pip install yamllint==1.13.0
  - pip install ansible-lint==3.5.1

Выводы

Статанализ разнообразен

many filters

Статанализ бесполезен при нерегулярном применении

muddy water

Фильтр грубой очистки ставится в начале каскада

rough filter

Используйте храповик

ratchet

Не забывайте про повторяемость сборок

repeatable

Ссылки

На этом всё!

ratchet