Тулинг для разработки на Lua

Навигация в коде

Я для написания кода использую vim и для навигации использую ctags. Чтобы ctags мог строить теги и для кода на Lua нужно добавить в конфиг .ctags такие строчки:

--langdef=lua
--langmap=lua:.lua
--regex-lua=/function[ \t]*[0-9a-zA-Z_\.\-]+[\.:](\w+)/\1/d,definition/

Форматирование кода

В мире Lua нет единого стиля написания кода, как это принято в мире гоферов. Каждый пишет так, как ему удобно или как принято в проекте. Если можно проверять форматирование кода автоматически, то почему бы это не делать? Для автоматического форматирования есть несколько инструментов:

Пакетный менеджер

Для управления зависимостями безальтернативный инструмент - luarocks. Вцелом он удобен для своих целей.

Измерение покрытия исходного кода тестами

Для измерения покрытия кода есть безальтернативный инструмент - luacov. С помощью отдельных модулей может создавать отчёты в разных форматах и в том числе в формате, пригодном для Coveralls. luacov сообщает только о покрытых строках, о покрытых ветках он ничего не знает.

Документация

LDoc позволяет генерировать документацию в HTML на основе аннотаций в коде. На выходе получается вот такая документация - https://tarantool.github.io/expirationd/.

Помимо LDoc есть locco, который позволяет писать код в стиле литературного программирования. На выходе вот такая документация - https://rgieseke.github.io/locco/.

Для знакомства с новым кодом я иногда использую штуки для посмтроения графа вызовов. В C/C++ это например можно сделать с помощью Doxygen и некоторые другие тулы. Для Lua есть luaflow, который рисует приличные картинки для графа вызовов в коде на Lua.

luaflow

Есть похожий проект на Питоне - code2flow, который строит граф вызовов, но он пока не поддерживает Lua (см. тикет #88).

Статический анализ

Для выявления проблем в коде без его запуска рекомендую luacheck. Бывало так, что забудешь закрыть скобку или другую опечатку сделаешь и во время запуска тестов luajit выдаёт совершенно непонятную ошибку. Запускаешь luacheck для проверки и выясняется, что забыл закрыть скобку или сделал другую досадную ошибку.

luacheck поддерживает ограниченный набор проверок, но иногда хочется искать ошибки, специфичные для какой-то библиотеки или проекта. Такое сделать тоже можно и мы для некоторых модулей в Тарантуле добавляли новые проверки. Проблема в точм, что проверки это код, который проверяет синтаксическое дерево и добавить даже примитивную проверку (например обязательно использование таймаутов в методах с вызовом по сети) занимает значительное время. Как решение такой проблемы можно использовать semgrep, он позволяет описывать правила в формате YAML и выявлять нужные места в коде с учётом семантики. semgrep поддерживает самые популярные языки программирования, а язык Lua в экспериментальном режиме. Анонс поддержки Lua был сделан в 2020 году, но с тех пор ничего не поменялось. Экспериментальный уровень поддержки означат, что парсер Lua разбирает 90% кода и из фич поддерживаются только concrete_syntax, deep_exprstmt, dots_args, dots_nested_stmts, dots_stmts, dots_string, metavar_arg, metavar_call, metavar_equality_var. Тем не менее даже с таким уровнем поддержки можно пользоваться semgrep в Lua. Я опубликовал свой набор правил для semgrep, с которыми можно искать наиболее популярные ошибки в Lua - https://github.com/ligurio/semgrep-rules/.

BMCLua - это интересный проект, который позволяет интегрировать Bounded Model Checking с кодом на Lua. Идея в том, чтобы использовать популярный чекер cbmc для выявления проблем в коде на Lua, BMCLua транслирует код на Lua в данные для чекера и запускает чекер для проверки инвариантов: арифметическое переполнение, деление на ноль.

BMCLua

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

luata-quickcheck - интересный проект по реализации статического анализатора для Lua. Если я правильно понял, то анализатор анализирует код, делает выводы о том, где какие типы данных используются и потом с помощью property-based тестов тестирует функции случайными данными этих типов. Основные идеи описаны в статье QuickChecking Static Analysis Properties.

Динамический анализ

Lua - это язык с динамической типизацией. Легко допустить ошибку, когда функция принимает аргумент одного типа, а мы передаём аргумент другого типа. Для выявления такого типа ошибок в Lua есть прекрасный модуль checks. В базовой реализации он очень простой, но тем не менее он незаменим при разработке крупных проектов.

-- Copyright (c) 2006-2013 Fabien Fleutot and others.
--
-- All rights reserved.

checkers = { }

local function check_one(expected, val)
    if type(val)==expected then return true end
    local mt = getmetatable(val)
    if mt and mt.__type==expected then return true end
    local f = checkers[expected]
    if f and f(val) then return true end
    return false
end

local function check_many(_, expected, val)
    if expected=='?' then return true
    elseif expected=='!' then return (val~=nil)
    elseif type(expected) ~= 'string' then
        error 'strings expected by checks()'
    elseif val==nil and expected :sub(1,1) == '?' then return true end
    for one in expected :gmatch "[^|?]+" do
        if check_one(one, val) then return true end
    end
    return false
end

local function checks(...)
    for i, arg in ipairs{...} do
        local name, val = debug.getlocal(2, i)
        local success = check_many(name, arg, val)
        if not success then
            local fname = debug.getinfo(2, 'n').name
            local fmt = "bad argument #%d to '%s' (%s expected, got %s)"
            local msg = string.format(fmt, i, fname or "?", arg, type(val))
            error(msg, 3)
        end
    end
end

return checks

Суть простая - в начале тела функции мы вставляем аннотации, которые описывают типы аргументов, которые может принимать функция. Если во время работы функции типы не совпадают с теми, которые описаны в аннотации, то генерируется ошибка. В аннотациях можно задавать названия стандартных типов, указывать типы из метаметода __type или добавлять проверки в виде функций:

function checkers.port(x)
    return type(x)=='number' and 0<=x and x<=0xffff and math.floor(x)==0
end

Есть несколько реализаций checks, расскажу о каждой из них:

  • checks на Си - https://github.com/fab13n/checks
  • checks на Lua - потерял ссылку на реализацию, но полный текст реализации приведен выше
  • форк checks для Tarantool, который добавляет типы данных Tarantool - https://github.com/tarantool/checks

Модуль strict включает в себя функции для включения или отключения строгого режима “strict mode”. Когда включен строгий режим, попытка использовать необъявленную глобальную переменную приведет к ошибке. Глобальная переменная считается необъявленной, если ей никогда не было присвоено значение. Часто это указывает на ошибку программирования. Пример реализации модуля strict можно найти на сайте lua.org - https://www.lua.org/extras/5.1/strict.lua.

require("strict")
print(a)       -- переменная 'a' не была объявлена ранее, поэтому будет сгенерирована ошибка

Тестирование

Большая часть проектов на Lua для тестирования это фреймворки для организации тестов наподобие pytest. Самый популярный в Lua это busted. Как по мне у него странная организация тестейсов в коде:

describe('Busted unit testing framework', function()
  describe('should be awesome', function()
    it('should have lots of features', function()
      -- deep check comparisons!
      assert.same({ table = 'great'}, { table = 'great' })

      -- or check by reference!
      assert.is_not.equals({ table = 'great'}, { table = 'great'})

      assert.falsy(nil)
      assert.error(function() error('Wat') end)
    end)
  end)
end)

Из тех, которые можно отметить, это luatest. Это форк luaunit для Tarantool, в него добавили вспомогательные функции, проверки, поддержку пропуска тесткейсов и может быть что-то ещё. Во всех модулях для Tarantool стараемся использовать luatest. По возможностям не дотягивает до pytest, но жить можно.

Для некоторых языков программирования есть инструменты для генерирования юнит тестов из кода программы. Для Lua подобного нет, хотя что-то похожее было описано в статье “Unit test code generator for lua programming language”. Там же указано, что такой генератор реализован a ZeroBrane Studio, IDE для Lua, но я не нашел в документации описания этой возможности.

Для Lua есть модуль для тестирования с помощью свойств - lua-quickcheck. До возможностей аналогичной библиотеки в Python Hypothesis ему ещё далеко, потому что реализованы только базовые возможности и в ближайшее время ничего не поменяется - автор не планирует модуль развивать.

Для использования фаззинга с обратной связью есть два проекта. Первый представляет из себя интерпретатор PUC Rio Lua с патчами для интеграции с популярным движком для фаззинга American Fuzzy Lop, afl-lua. Второй представляет из себя Lua-модуль для интеграции популярной библиотеки для фаззинга C/C++ libFuzzer с Lua, luzer. В обоих проектах используется обратная связь, что делает тестирование эффективнее по сравнению с lua-quickcheck.

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

Смотри ещё

Список библиотек и проектов на Lua - https://github.com/LewisJEllis/awesome-lua#analysis-tools-and-asts

Доклад моего коллеги “Tarantool team’s experience with Lua developer tools”, слайды, видео. Мой пост покрывает все, перечисленные в докладе инструменты.

Доклад другого моего коллеги Тимура Сафина про тулинг в Lua - видео. Доклад был в контексте сравнения тулинга для Питона и для Lua.

Подробнее про проверку типов в Lua - http://lua-users.org/wiki/LuaTypeChecking

Подробнее про анализ программ на Lua - http://lua-users.org/wiki/ProgramAnalysis

Теги: feedsoftware