Рубрики
Без рубрики

Анализ статического кода: что это такое? Как это использовать?

Основная теория статического анализа кода и руководящим руководством о том, как написать свои собственные анализаторы для автоматического выявления проблем в коде. Помечено Python, учебник, AST, Codequality.

Анализ статического кода относится к методике аппроксимации поведения времени выполнения программы. Другими словами, это процесс прогнозирования выпуска программы без на самом деле выполняет его.

В последнее время, однако, термин «анализ статического кода» более часто используется для обозначения одного из приложений этой методики, а не самой техникой – Понимание программы – Понимание программы и обнаружения проблем в нем (что-либо из синтаксических ошибок для типовых несоответствий, производительности свиней, вероятно, ошибки, лазейки безопасности и т. Д.). Это использование, которое мы имели в виду на протяжении всего этого поста.

” Уточнение методов для быстрого обнаружения ошибок служит а также любым другим как отличительной чертой того, что мы имеем в виду по науке ».

– Дж. Роберт Оппенгеймер

Контур

Мы охватываем много земли в этом посте. Цель состоит в том, чтобы создать понимание анализа статического кода и оснастить вас основной теорией, а также правильными инструментами, чтобы вы могли самостоятельно написать анализаторы.

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

Обратите внимание, что хотя идеи здесь обсуждаются в свете Python, анализаторы статического кода на всех языках программирования вырезают вдоль похожих строк. Мы выбрали Python из-за наличия простых в использовании AST Модуль и широкое принятие самого языка.

Как все это работает?

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

Как вы можете видеть на диаграмме (продолжать, увеличить его!), Статические анализаторы питаются на выходе этих этапов. Чтобы быть в состоянии лучше понять методы статического анализа, давайте посмотрим на каждый из этих шагов еще несколько подробностей:

Сканирование

Первое, что делает компилятор при попытке понять кусок кода, состоит в том, чтобы разрушить его на мелкие куски, также известные как токены. Токены сродни, к каким словам на языке.

Токен может состоять из одного символа, как ( или литералы (как целые числа, струны, например, 7 , Боб и т. Д.), Или зарезервированные ключевые слова этого языка (например, def в Python). Персонажи, которые не способствуют семантике программы, как конечный пробел, комментарии и т. Д. Часто отбрасываются сканером.

Python предоставляет токенизировать Модуль в своей стандартной библиотеке, чтобы позволить вам играть с токенами:

import io
import tokenize

code = b"color = input('Enter your favourite color: ')"

for token in tokenize.tokenize(io.BytesIO(code).readline):
    print(token)
TokenInfo(type=62 (ENCODING),  string='utf-8')
TokenInfo(type=1  (NAME),      string='color')
TokenInfo(type=54 (OP),        string='=')
TokenInfo(type=1  (NAME),      string='input')
TokenInfo(type=54 (OP),        string='(')
TokenInfo(type=3  (STRING),    string="'Enter your favourite color: '")
TokenInfo(type=54 (OP),        string=')')
TokenInfo(type=4  (NEWLINE),   string='')
TokenInfo(type=0  (ENDMARKER), string='')

(Обратите внимание, что ради читабельности я пропускал несколько столбцов из результата выше – метаданные, такими как начальный индекс, окончательный индекс, копию линии, на которой происходит токен и т. Д.)

Разборка

На этом этапе у нас есть только словарь языка, но сами жетоны не отражают ничего о грамматике языка. Именно здесь парсер вступает в игру.

Парсер берет эти токены, подтверждает, что последовательность, в которой они кажутся соответствуют грамматике и организуют их в древовидной структуре, представляющей структуру высокого уровня программы. Это удачно называется Абстрактное синтаксическое дерево (АСТ).

«Аннотация», потому что это тезисы от низкоуровневых незначительных деталей, таких как скобка, отступ и т. Д., позволяя пользователю сосредоточиться только на Логическая структура Из программы – это то, что делает его наиболее подходящим выбором для проведения статического анализа на.

Анализируя АСТС

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

Python Ships с AST Модуль как часть его стандартной библиотеки, которую мы будем использовать в значительной степени во время написания анализаторов позже.

Если у вас нет предыдущего опыта работы с AUTS, вот как AST Модуль работает:

  • Все типы узлов AST представлены соответствующей структурой данных в AST Модуль, например, для петлей характеризуются АСТ. Для объект.
  • Для создания AST из исходного кода мы используем AST.PARSE функция.
  • Для анализа синтаксического дерева нам нужен AST «Walker» – объект для облегчения прохождения дерева. AST Модуль предлагает два ходунка:

    • АСТ. Nodevisitor (не разрешает модификацию на входное дерево)
    • АСТ. NodeTransformer (позволяет модифицировать)
  • При пересечении синтаксического дерева мы обычно заинтересованы только в анализе нескольких узлов, представляющих интерес, например, Если мы пишем анализатор, чтобы предупредить нас, если у нас будет более 3 вложенных для петель, мы будем интересоваться только посещением АСТ. Для узлы.
  • Для анализа определенного типа узла ходьбе нужно реализовать специальный метод. Этот метод часто называют методом «посетителей». Терминология: чтобы посетить узел, то это не что иное, как только призыв к этому методу.
  • Эти методы называются как посетить_ + , например, добавить посетитель для «для петлей», метод должен быть назван Visit_for Отказ
  • Есть верхний уровень посетить Метод, который рекурсивно посещает входной узел, то есть он первым посещает себя, то все его дети узлы, затем дети узлы детей узлов, так и так далее.

Просто чтобы дать вам ощущение того, как это работает, давайте напишем код для посещения всех для циклов:

import ast

# Demo code to parse
code = """\
sheep = ['Shawn', 'Blanck', 'Truffy']

def get_herd():
    herd = []
    for a_sheep in sheep:
        herd.append(a_sheep)
    return Herd(herd=herd)

class Herd:
    def __init__(self, herd):
        self.herd = herd

    def shave(self, setting='SMOOTH'):
        for sheep in self.herd:
            print(f"Shaving sheep {sheep} on a {setting} setting")
"""


class Example(ast.NodeVisitor):
    def visit_For(self, node):
        print(f"Visiting for loop at line {node.lineno}")

tree = ast.parse(code)
visitor = Example()
visitor.visit(tree)

Это выходы:

Visiting for loop at line 5
Visiting for loop at line 14
  • Сначала мы посещаем верхний уровень АСТ. Модуль узел.
  • Поскольку ни один посетитель не существует для этого узла, по умолчанию, посетитель начинает посещать своих детей – АСТ. Назначить С АСТ. FunctionDef и АСТ. ClassDef узел.
  • Поскольку для них не существует никаких посетителей, посетитель снова начинает посещать всех своих детей.
  • На некотором этапе, когда АСТ. Для цикл наконец встречается, Visit_for Способ называется. Обратите внимание, что копия Узел также передается на этот метод – который содержит все метаданные об этом – дети (если есть), номер линии, столбец и т. Д.

У Python также есть несколько других сторонних модулей, таких как астроид , astmonkey , Астор которые предоставляют дополнительные абстрактные модули для облегчения нашей жизни.

Но, в этом посту, мы ограничимся Barebones AST Модуль, так что мы увидим реальные, уродливые операции за кулисами.

Примеры

Хотя этот пост блога – это только введение в анализ статического кода, мы будем писать скрипты для обнаружения проблем, которые очень актуальны в реальных сценариях, а также (шансы, что ваша IDE уже предупреждает вас, если вы нарушаете один). Это показывает, насколько мощный анализ статического кода, и что он позволяет делать с таким небольшим количеством кода:

  • Обнаружение любого использования одинарных кавычек вместо двойных кавычек.
  • Обнаружить, если Список () используется вместо []
  • Обнаружить слишком много вложенных для петлей.
  • Обнаружение неиспользованного импорта в файле.

Вот как примеры будут работать:

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

Обнаружение одиночных кавычек

Здесь мы пишем сценарий, который поднимет предупреждение всякий раз, когда он обнаруживает, что отдельные кавычки были использованы в файлах Python, приведенные в качестве ввода.

Этот пример может считаться элементарным по сравнению с другими современными методами анализа статического кода, но он все еще включен здесь из-за исторического значения – это было в значительной степени, как работали ранние анализаторы кода 1 Отказ Еще одна причина имеет смысл включить эту технику вот то, что он сильно используется многими популярными статическими инструментами, как черный Отказ

import sys
import tokenize


class DoubleQuotesChecker:
    msg = "single quotes detected, use double quotes instead"
    def __init__(self):
        self.violations = []

    def find_violations(self, filename, tokens):
        for token_type, token, (line, col), _, _ in tokens:
            if (
                token_type == tokenize.STRING
                and (
                    token.startswith("'''")
                    or token.startswith("'")
                )
            ):
                self.violations.append((filename, line, col))

    def check(self, files):
        for filename in files:
            with tokenize.open(filename) as fd:
                tokens = tokenize.generate_tokens(fd.readline)
                self.find_violations(filename, tokens)

    def report(self):
        for violation in self.violations:
            filename, line, col = violation
            print(f"{filename}:{line}:{col}: {self.msg}")


if __name__ == '__main__':
    files = sys.argv[1:]
    checker = DoubleQuotesChecker()
    checker.check(files)
    checker.report()

Вот расстройство того, что происходит:

  • Имена входных файлов читаются как аргументы командной строки.
  • Эти имена файлов передаются на Проверьте Метод, который генерирует токены для каждого файла и передает их на Find_Violations метод.
  • Find_Violations Способ передавать список токенов и ищет токенов «Тип строки», значение которых является либо « или ' Отказ Если он находит один, он флагирует строку, добавив ее на Self.Violations. .
  • отчет Метод затем читает все проблемы из Self.Violations и печатает их с помощью сообщения о полезных ошибок.
def simulate_quote_warning():
    '''
    The docstring intentionally uses single quotes.
    '''
    if isinstance(shawn, 'sheep'):
        print('Shawn the sheep!')
example.py:2:4: single quotes detected, use double quotes instead
example.py:5:25: single quotes detected, use double quotes instead
example.py:6:14: single quotes detected, use double quotes instead

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

Котельная для дальнейших примеров

Предыдущий пример был единственным примером, где мы работали непосредственно с токенами. Для всех остальных мы ограничили наше взаимодействие только к генерируемым ASTS.

Поскольку множество кода будет дублирован через эти шашки, и этот пост уже так долго, давайте сначала получим какой-то код Bovertlate на месте, который мы можем позже повторно использовать для всех примеров. Определение кода Code Code также позволяет мне обсудить только соответствующие данные под каждым контролем и сойти со всей бизнес-логикой одновременно:

import ast
from collections import defaultdict
import sys
import tokenize


def read_file(filename):
    with tokenize.open(filename) as fd:
        return fd.read()

class BaseChecker(ast.NodeVisitor):
    def __init__(self):
        self.violations = []

    def check(self, paths):
        for filepath in paths:
            self.filename = filepath
            tree = ast.parse(read_file(filepath))
            self.visit(tree)

    def report(self):
        for violation in self.violations:
            filename, lineno, msg = violation
            print(f"{filename}:{lineno}: {msg}")

if __name__ == '__main__':
    files = sys.argv[1:]
    checker = ()
    checker.check(files)
    checker.report()

Большая часть кода работает так же, как мы видели в предыдущем примере, за исключением того, что:

  • У нас новая функция read_file читать содержимое данного файла.
  • Проверьте Метод, вместо токенизации, считывает содержимое всех файловых путей один за другим, а затем анализирует его AST, используя AST.PARSE метод. Затем он использует посетить Метод для посещения узла верхнего уровня (AS AST. Модуль Итак, тем самым все его дети узлы рекурсивно. Это также устанавливает значение Self.filename Для анализа текущего файла – так что мы можем добавить имя файла в сообщении об ошибке, когда мы найдем нарушение позже.

Вы можете заметить, что есть пара неиспользованного импорта – они будут использоваться позже. Кроме того, заполнитель Необходимо заменять фактическое имя класса Checker при запуске кода.

Для всего готов к запуску кода для всех шашек в этом посте см. этот гитбеб

Обнаружение использования списка ()

Рекомендуется использовать пустой буквальный [] вместо Список () для пустого списка, потому что он имеет тенденцию быть медленнее – имя Список Должно быть посмотрел в глобальную область, прежде чем позвонить ему. Кроме того, это может привести к ошибке на случай, если имя Список отскок к другому объекту.

Список () проживает как АСТ. Позвоните узел. Таким образом, мы начинаем с определения visit_call Метод для нашего нового Listdefinitionchecker сорт:

class ListDefinitionChecker(BaseChecker):
    msg = "usage of 'list()' detected, use '[]' instead"

    def visit_Call(self, node):
        name = getattr(node.func, "id", None)
        if name and name == list.__name__ and not node.args:
            self.violations.append((self.filename, node.lineno, self.msg))

Вот кратко, что мы делаем:

  • При посещении Позвоните Узел, мы сначала стараемся получить название названия функции.
  • Если он существует, мы проверяем, равно ли это Список .__ Имя__ Отказ
  • Если да, мы теперь уверены, что звонок на Список (...) делается.
  • После этого мы гарантируем, что никакие аргументы не передаются на Список Функция, то есть сделанный звонок действительно Список () Отказ Если это так, мы отстаем эту строку, добавив проблему.

Запустив этот файл в одном примере кода (убедитесь, что вы обновили в котельной к ListDefinitionChecker ):

def build_herd():
    herd = list()
    for a_sheep in sheep:
        herd.append(a_sheep)
    return Herd(herd)
example.py:2: usage of 'list()' detected, use '[]' instead

Обнаружение слишком много вложенных для петлей

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

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

Вот что мы будем делать: мы начинаем рассчитывать, как только АСТ. Для Узел встречается. Мы также отмечаем этот узел как «родительский» узел. Мы, тогда проверяем, есть ли какие-либо из его детей АСТ. Для узлы. Если да, мы увеличиваем количество и повторите одну и ту же процедуру для узема ребенка снова.

class TooManyForLoopChecker(BaseChecker):
    msg = "too many nested for loops"

    def visit_For(self, node, parent=True):
        if parent:
            self.current_loop_depth = 1
        else:
            self.current_loop_depth += 1

        for child in node.body:
            if type(child) == ast.For:
                self.visit_For(child, parent=False)

        if parent and self.current_loop_depth > 3:
            self.violations.append((self.filename, node.lineno, self.msg))
            self.current_loop_depth = 0

Рабочий процесс может выглядеть немного перекошено сначала, но вот в основном то, что мы делаем:

  • Когда посетить Способ называется (из basechecker класс), он начинает искать любой АСТ. Для узлы в АСТ. Как только он находит один, он называет методом Visit_for С помощью ключевого слова по умолчанию родитель = правда Отказ
  • Мы используем переменную родитель Как флаг для отслеживания внешней петли – в этом случае мы инициализируем self.current_loop_depth До 1, иначе мы просто увеличиваем его значение на 1.
  • Мы изучаем тело этого цикла, чтобы рекурсивно искать любого ребенка АСТ. Для узлы. Если мы найдем один, мы называем Visit_for с родитель = Ложь Отказ
  • Когда мы закончим прохождение, мы оцениваем, достигнута ли глубина петли за пределами 3. Если это так, мы сообщаем о нарушении и сбросите глубину петли до 0 снова.

Давайте запустим наш сценарий на некоторые примеры:

for _ in range(10):
    for _ in range(5):
        for _ in range(3):
            for _ in range(1):
                print("Baa, Baa, black sheep")

for _ in range(4):
    for _ in range(3):
        print("Have you any wool?")

for _ in range(10):
    for _ in range(5):
        for _ in range(3):
            if True:
                for _ in range(3):
                    print("Yes, sir, yes, sir!")
example.py:1: too many nested for loops

Вы заметили предостережение здесь? Если вложенное для цикла не является прямым ребенком родительской петли, он никогда не посещается, и, следовательно, не сообщается. Тем не менее, получение нашего кода для работы на этом краевом случае unains и отсутствует в этом посте.

Обнаружение неиспользованного импорта

Обнаружение неиспользованного импорта отличается от предыдущих случаев, потому что мы не можем немедленно отменить нарушения во время посещения узлов – у нас нет полной информации о том, что все «имена» будут использоваться во всем модуле. Поэтому мы реализуем этот анализатор в двух проходах:

  • В первом проходе мы проходим все узлы, где могут быть определены импорт ( AST. Импорт , АСТ. Импорт из ), собирая имена всех модулей, которые были импортированы.
  • В том же проходе мы также заполняем множество со всеми именами, которые используются в этом файле, реализуя посетитель для АСТ. Имя Отказ
  • Во втором проходе мы видим, какие имена были импортированы, но не используются. Затем мы распечатаем сообщение об ошибке для всех таких имен.
class UnusedImportChecker(BaseChecker):
    def __init__(self):
        self.import_map = defaultdict(set)
        self.name_map = defaultdict(set)

    def _add_imports(self, node):
        for import_name in node.names:
            # Store only top-level module name ("os.path" -> "os").
            # We can't easily detect when "os.path" is used.
            name = import_name.name.partition(".")[0]
            self.import_map[self.filename].add((name, node.lineno))

    def visit_Import(self, node):
        self._add_imports(node)

    def visit_ImportFrom(self, node):
        self._add_imports(node)

    def visit_Name(self, node):
        # We only add those nodes for which a value is being read from.
        if isinstance(node.ctx, ast.Load):
            self.name_map[self.filename].add(node.id)

    def report(self):
        for path, imports in self.import_map.items():
            for name, line in imports:
                if name not in self.name_map[path]:
                    print(f"{path}:{line}: unused import '{name}'")
  • Всякий раз, когда Импорт или Импорт из Узел встречается, мы храним его имя в наборе.
  • Чтобы получить набор всех названий, используемых в файле, мы посещаем АСТ. Имя Узлы: для каждого такого узла мы проверяем, прочитается ли значением значение, которое подразумевает, что ссылка на уже существующее имя, а не создает новый объект. (Если это имя импорта, он должен уже существовать) – если да, мы добавляем имя на набор.
  • отчет Метод проходит через список всех импортных имен в файле и проверяет, присутствуют ли они в наборе используемых имен. Если нет, он печатает сообщение об ошибке сообщение о нарушении.

Давайте пойдем вперед и запустим этот скрипт на несколько примеров:

import antigravity
import os.path.join
import sys
import this

tmpdir = os.path.join(sys.path[0], 'tmp')
example.py:1: unused import 'antigravity'
example.py:4: unused import 'this'

Обратите внимание, что ради краткости я пошел с простейшей версией кода возможным. Этот выбор имеет побочный эффект, который наш код не обрабатывает некоторые сложные угловые случаи (например, когда импорт псевдоним – Импортировать Foo в качестве бара или когда имя читается из Locals () Dict , так далее.).

Фу! Это был целый список вещей, чтобы обернуть голову. Но наша награда заключается в том, что в следующий раз мы определим шаблон вызываемого ошибка-кода, мы можем идти прямо вперед и написать сценарий для автоматического обнаружения его.

использованная литература

  1. Безопасное программирование со статическим анализом: Шахматы, Брайан, Запад, Джейкоб
  2. Pylint – Python Статический код Анализ кода – Gilad Shefer – Pycon Israel 2019
  3. Ремесление переводчиков Боб Нистремом
  4. Стервятник Джендрика Сейпп

[Этот пост изначально появился на Deepsource.io/blog Несомненно

  1. Они отсканировали код, который ищет звонки в такие функции, как strcpy () Это было легко неправильно использовать и должно было быть проверено как часть ручного обзора исходного кода. ↩

Оригинал: “https://dev.to/deepsource/static-code-analysis-what-it-is-how-to-use-it-4b79”