Skip to content

TNT-Bots/lua-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

Стиль программирования на LuaJIT/Tarantool

Это руководство по стилю программирования на Tarantool (LuaJIT), которого я стараюсь придерживаться в своих проектах. Часть правил опирается на API Tarantool (box.NULL, расширения модуля table, метаметод __serialize) и к чистому LuaJIT неприменима — такие места помечены. Основная цель — прийти к единой согласованности между проектами, а также подобрать наиболее комфортный стиль, при котором будут совмещены удобство чтения, чёткое разделение абстракций и отсутствие запутанной логики.

В качестве ориентира были взяты лучшие практики (субъективно) из следующих руководств:


Оглавление

  1. Таблицы
  2. Строки
  3. Функции
  4. Циклы
  5. Свойства
  6. Переменные
  7. Условные выражения
  8. Блоки
  9. Комментарии
  10. Пробелы
  11. Запятые
  12. Разделитель
  13. Преобразование типов
  14. Именования
  15. Конструкторы
  16. Модули
  17. Метаметоды
  18. Структура файлов
  19. Обработка ошибок
  20. Статический анализ
  21. Тестирование
  22. Производительность

  • Составные типы (table, function, userdata) передаются по ссылке, примитивные — по значению. Присваивание не копирует таблицу.
-- Примитивные типы передаются по значению
local foo = 1
local bar = foo -- bar = 1, копия

-- Составные типы передаются по ссылке
local foo = { 1, 2, 3 }
local bar = foo -- bar ссылается на ту же таблицу
  • Для копирования таблиц используйте table.copy (поверхностное) и table.deepcopy (глубокое), не пишите свою реализацию. Это расширения Tarantool — в чистом LuaJIT их нет.

  • По умолчанию создавайте таблицы через литерал.

Обоснование: Литерал декларативнее, а в LuaJIT ещё и быстрее — конструктор компилируется с преаллокацией под известное число полей, последовательные присваивания растят таблицу инкрементально.

-- Хорошо
local index = {
  name = 'primary',
  options = { parts = { 'id' }, unique = true },
}
  • Исключение — модели с валидацией: заполняйте их поля последовательно, чтобы между присваиваниями вставлять проверки и накапливать ошибки (см. Конструкторы).
local model = {}
model.id = tonumber(data.id)
model.name = tostring(data.name)
model.locked = data.locked or false
  • Управление сериализацией таблиц (например, пустая таблица как объект, а не массив) — через метаметод __serialize, см. Метаметоды.
  • Используйте одинарные кавычки '' для строк.
-- Плохо
local name = "John"

-- Хорошо
local name = 'John'
  • При конкатенации переносите оператор на новую строку.
-- Плохо
local infoStr = 'Info: ' .. 'Host: ' .. '0.0.0.0' .. ' ' .. 'Port: ' .. 8080

-- Хорошо
local infoStr = 'Info: '
  .. 'Host: ' .. '0.0.0.0' .. ' '
  .. 'Port: ' .. 8080
  • Не используйте \z для переноса текста на новую строку.
-- Плохо
-- Текст с переносами, собранный через \z, тяжело читать и править.
local text = '\z
First Name: Alex\z
\nLast Name: Wazowsky\z
\nAge: 27\z
'

-- Хорошо: текст вынесен в двойные квадратные скобки
local text = [[
First Name: Alex
Last Name: Wazowsky
Age: 27]]
  • Всегда используйте круглые скобки при вызове функций.
-- Плохо
local m = require 'module'
print 'hello'

-- Хорошо
local m = require('module')
print('hello')
  • Предпочитайте следующий стиль функций.
-- Плохо
local foo = function()
  -- ...
end

-- Хорошо
local function foo()
  -- ...
end
  • Для методов сервисов используйте запись через точку, без self.
local service = {}

function service.create(data)
  -- ...
end

function service.read(id)
  -- ...
end

return service
  • Для методов, которым нужен доступ к self, используйте запись через двоеточие.
function Errors:field_missing(field)
  self.count = self.count + 1
  -- ...
end
  • Функции, возвращающие результат с возможной ошибкой, используют паттерн return value, error.
--- Получить пользователя по id.
-- @tparam number id id пользователя
-- @treturn[1] table user
-- @treturn[2] table err
function service.read(id)
  local user, err = sql(query, { id = id })
  if err then
    return nil, err
  end

  return user, nil
end
  • Избегайте длинных списков аргументов. Если параметров больше трёх — передавайте таблицу.
-- Плохо
local function createUser(name, email, role, locked, avatarUrl)
  -- ...
end

-- Хорошо
local function createUser(data)
  -- data.name, data.email, data.role, ...
end
  • Для массивов (числовой индекс) предпочитайте for i = 1, #tbl do вместо ipairs.
for i = 1, #routes do
  local route = routes[i]
  httpd:route(route.options, route.handler)
end
  • Для итерации по парам ключ-значение используйте pairs.
for key, val in pairs(tbl) do
  -- ...
end
  • Если значение индекса не используется, именуйте его _.
for _, val in pairs(tbl) do
  process(val)
end
  • Используйте точечную нотацию для доступа к известным свойствам.
-- Хорошо
local name = user.first_name
local id = user.id
  • Используйте скобочную нотацию [] для доступа по динамическому ключу.
local field = 'first_name'
local value = user[field]
  • Всегда используйте local для объявления переменных. Глобальные переменные не допускаются (за исключением Tarantool API: box, _TARANTOOL).
-- Плохо
name = 'John'

-- Хорошо
local name = 'John'
  • Объявляйте каждую переменную на отдельной строке. Правило касается независимых присваиваний — приём нескольких возвращаемых значений (local user, err = service.read(id)) под него не попадает.
-- Плохо
local a, b = 1, 2

-- Хорошо
local a = 1
local b = 2
  • Объявляйте переменные как можно ближе к месту использования.

  • Используйте box.NULL вместо nil при работе с Tarantool-данными, где нужно различать "нет значения" и "значение не задано".

if data.username ~= box.NULL then
  model.username = tostring(data.username)
end
  • Используйте ранний выход (guard clause) вместо глубокой вложенности.
-- Плохо
local function process(data)
  if data then
    if data.id then
      -- ...
    end
  end
end

-- Хорошо
local function process(data)
  if not data then
    return nil
  end

  if not data.id then
    return nil
  end

  -- ...
end
  • Для значений по умолчанию используйте or.
local locked = data.locked or false
local name = data.name or 'Unknown'
  • Для проверки наличия опционального параметра используйте and.
local init = opts and opts.init
  • При работе с данными из Tarantool используйте явное сравнение с nil вместо проверки truthiness.

Обоснование: box.NULL — это cdata, в условных выражениях он truthy, поэтому if x then его пропустит. При этом box.NULL == nil истинно (NULL-указатель cdata в LuaJIT равен nil), так что сравнения ~= nil достаточно, чтобы отсечь и nil, и box.NULL.

-- Опасно при работе с Tarantool
if data.field then
  -- box.NULL пройдёт эту проверку
end

-- Хорошо: отсекает и nil, и box.NULL
if data.field ~= nil then
  -- ...
end
  • Предпочитайте позитивные проверки негативным, если есть обе ветки.
-- Плохо
if not thing then
  handleMissing()
else
  handlePresent()
end

-- Хорошо
if thing then
  handlePresent()
else
  handleMissing()
end
  • Допускается использование not (x == y) вместо x ~= y, когда это делает код читабельнее.
  • Блоки if, for, while, function всегда завершаются end на отдельной строке.
if condition then
  -- ...
end

for i = 1, 10 do
  -- ...
end
  • Не используйте однострочные блоки.
-- Плохо
if condition then return end

-- Хорошо
if condition then
  return
end
  • Язык комментариев — русский.

  • Пробел после --.

--плохо
-- хорошо
  • В комментариях используйте только ASCII-пунктуацию. Не используйте типографские символы (, , ×, кавычки-«ёлочки») — заменяйте их дефисом -, стрелкой ->, буквой x, обычными кавычками.
-- Плохо
-- avatar_url — обновляет только админ. Переход planned → active.

-- Хорошо
-- avatar_url - обновляет только админ. Переход planned -> active.
  • Документация функций оформляется в стиле LuaDoc/LDoc: первая строка через ---, остальные через --. Документация генерируется LDoc (./bin/ldoc .), поэтому докблоки должны быть валидны для него.
--- Получить пользователя по id.
-- @tparam number id id пользователя
-- @treturn[1] table user
-- @treturn[2] table err
function service.read(id)
  • Правила, продиктованные парсером LDoc:

    • Первая строка докблока — законченное предложение с точкой на конце. LDoc считает summary весь текст до первой точки, и без неё в summary склеиваются следующие строки.
    • Первый комментарий файла — всегда ----заголовок модуля. Если файл начинается с обычного --, LDoc возьмёт описанием модуля первый попавшийся ----докблок из середины файла.
    • Докблок функции стоит непосредственно над ней, без пустых строк внутри блока и между блоком и функцией. Пустая строка разрывает блок, и LDoc теряет привязку к функции.
    • Заголовок модуля и докблок функции — разные блоки. Если единственный докблок файла стоит над функцией, он уйдёт в описание модуля, а функция останется без документации.
    • @module не указывать: имя модуля LDoc выводит из пути файла (services.peaks.search).
    • @deprecated не использовать (LDoc не знает такой тег) — писать словами: -- Устарело: ....
  • Доступные LuaDoc-теги:

Тег Формат Описание
@tparam @tparam type name описание Параметр функции с типом
@tparam[opt] @tparam[opt] type name описание Опциональный хвостовой параметр ([opt=значение] — со значением по умолчанию)
@treturn[1] @treturn[1] type name/описание Возврат при успехе
@treturn[2] @treturn[2] table err Возврат при ошибке (группы [1]/[2] рендерятся как «Or»)
@treturn @treturn type описание Возврат без групп (функция не возвращает ошибку)
@usage блок с примером Пример использования
@see @see name Ссылка на другой документированный item
  • Типы: базовые Lua-типы (string, number, boolean, table, function) и доменные (unsigned, cdata, datetime, uuid). Тип — одно слово без пробелов; объединение через | (string|number); ?type означает «type или nil»; поля таблицы-опций документируются точечными именами (@tparam string opts.space имя спейса).

  • Пример с @usage (строки примера — на уровне --, без дополнительного отступа):

--- Выполнить SQL-запрос.
-- @tparam string sql_query SQL-строка
-- @tparam table values таблица значений
-- @treturn[1] table result
-- @treturn[2] table err
-- @usage
-- local rows = sql.execute('SELECT * FROM users WHERE name = ${name}', { name = 'Alex' })
-- if rows == nil then
--   log.error('No rows')
-- end
  • Константные таблицы (энамы) документируются докблоком прямо над local <name> = {, поля — inline-комментариями после значений; LDoc собирает такую таблицу с полями автоматически. Отдельные ---строки внутри конструктора недопустимы: LDoc приклеит их к описанию предыдущего поля.
--- Энам ролей.
--

--- Роли пользователей.
local roles = {
  USER = 'user', -- обычный пользователь
  ADMIN = 'admin', -- администратор
}
  • Для визуального разделения секций используйте блоки комментариев.
-- ----------------------------
-- Точка входа
-- ----------------------------
  • Для примеров данных (JSON-запросы/ответы) используйте блочные комментарии --[[ ... --]].
--[[ Данные для регистрации
{
  "phone": "+71234567890",
  "email": "foo@bar.baz", -- Опционально
  "password": "qwerty",
  "first_name": "John",
  "last_name": "Doe"
}
--]]
  • Используйте TODO и FIXME теги. TODO — для запланированной доработки, FIXME — для известной проблемы.
-- TODO: реализовать пагинацию
-- FIXME: неэффективный запрос, нужна оптимизация
  • Для ссылок на внешние ресурсы используйте See:.
-- See: https://www.tarantool.io/en/doc/latest/reference/...
  • Директивы luacheck оформляются как комментарии.
-- luacheck: ignore req
-- luacheck: push ignore 631
-- ... код ...
-- luacheck: pop
  • Пустая строка-комментарий -- используется как визуальный разделитель внутри блока кода.
-- Проверка, что пользователя с таким телефоном нет
--
local existing, err = authService.findByPhone(data.phone)
if err then
  return nil, err
end
--
  • В многострочных комментариях переносите строку по смысловым границам, а не по ширине — не разрывайте словосочетание посреди мысли.
-- Плохо: перенос рвёт фразу "нормальный путь"
-- avatar_url через эту ручку может писать только админ; нормальный
-- путь для юзера - загрузка через media-service.

-- Хорошо: каждая строка читается цельным фрагментом
-- avatar_url - обновлять может только админ.
-- Стандартный путь для юзера - загрузка через media-service,
-- который сам зовёт POST /internal/users/:id/avatar.
  • Перечисление случаев внутри многострочного комментария оформляйте списком: один случай — одна строка, -- подчинённых строк сдвигается на отступ. Не склеивайте случаи в сплошной абзац, заполненный по ширине.
-- Плохо: случаи склеены в абзац, перенос по ширине рвёт фразы
-- Глагол навязывает лицо и время: "создали" - ещё ничего не
-- создано, "создаём" - действие выполняет код, а не автор.

-- Хорошо: структура аргумента видна по сетке строк
-- Глагол навязывает лицо и время, и любой выбор ложен:
  -- "создали" - в точке чтения ещё ничего не создано.
  -- "создаём" - действие выполняет код, а не автор с читателем.

Обоснование: Комментарий сканируют, а не читают подряд. Плотный абзац скрывает структуру аргумента, список делает её видимой: симметричные случаи стоят на симметричных строках, и их можно пропустить, ухватив утверждение и вывод.

  • Одна мысль — одно предложение. Не склеивайте независимые утверждения через ;, разносите на отдельные предложения с точкой.
-- Плохо
-- Оценку ставит владелец оператора; повторно оценить нельзя.

-- Хорошо
-- Оценку ставит владелец оператора.
-- Повторно оценить нельзя.
  • Комментарий, описывающий шаг алгоритма (действие блока кода ниже), формулируйте отглагольным существительным, а не глаголом: "Создание пользователя", а не "Создаём пользователя" или "Создали пользователя". На комментарии-утверждения ("Оценку ставит владелец оператора.") правило не распространяется.
-- Плохо
-- Сначала находим запись
-- Добавили гида в участники тура

-- Хорошо
-- Поиск записи
-- Добавление гида в участники тура

Обоснование: Глагол навязывает лицо и время, и любой выбор ложен:

  • "создали" - в точке чтения ещё ничего не создано;
  • "создаём" - действие выполняет код, а не автор с читателем.

Отглагольное существительное нейтрально ко времени и лицу: комментарий читается как заголовок шага, единообразно с разделителями секций.

  • Комментарий к конкретному полю начинайте с имени поля: -- <field> - <суть>.
-- Хорошо
-- avatar_url - обновлять может только админ.
-- rating - целое от 1 до 5.
  • TODO / FIXME пишите одной строкой с сутью задачи. Пояснение или последствие выносите отдельной строкой --, без выравнивающих отступов (hanging-indent) и без склейки через ;.
-- Плохо
-- TODO: валидация формата id; сейчас
--       админ может вписать произвольную строку и сломать рендер.

-- Хорошо
-- TODO: валидация формата id (^users/<uid>/<uuid>$ либо '').
-- Сейчас админ может вписать произвольную строку и сломать рендер.

Обоснование: hanging-indent и склейка через ; мешают чтению и правке. Исключение — структурированные блоки (нумерованные шаги, выровненные таблицы переходов вида -- a -> b - описание), где выравнивание осмысленно.

  • Не комментируйте синтаксис — читатель должен знать Lua. Комментируйте намерение и поведение.
-- Плохо
-- Присваиваем переменной x значение 1
local x = 1

-- Хорошо
-- Начальное смещение для пагинации
local x = 1
  • Комментарий объясняет, что делает код и зачем, а не как он работает.

Обоснование: Если код требует объяснения "как" — перепишите код так, чтобы он читался без комментария. Пересказ механики устаревает при первой же правке кода и превращается в ложь.

-- Плохо: пересказ механики
-- Идём по маршрутам в цикле и для каждого вызываем httpd:route
for i = 1, #routes do
  httpd:route(routes[i].options, routes[i].handler)
end

-- Хорошо: что и зачем
-- Регистрация маршрутов до старта httpd - после старта роутинг не изменить
for i = 1, #routes do
  httpd:route(routes[i].options, routes[i].handler)
end
  • Комментарии хороши, но избыток комментирования вреден. Не комментируйте тело функции построчно — основное описание ("что" и "почему") давайте в заголовке функции (LuaDoc). Внутри тела допустимы короткие пометки для неочевидных или вынужденно некрасивых мест.

Обоснование: Если тело функции требует отдельных комментариев по частям, функция слишком сложная. Разбейте её на более простые, а не объясняйте комментариями.

  • Комментируйте объявления данных: константы, элементы перечислений, поля моделей. Одно объявление на строку — чтобы оставалось место для комментария.
-- Хорошо
local MAX_RETRIES = 3 -- Лимит повторов транзакции при конфликте

local roles = {
  USER = 1,  -- Обычный пользователь
  GUIDE = 2, -- Гид, может создавать туры
  ADMIN = 3, -- Полный доступ
}
  • Используйте 2 пробела для отступов. Не используйте табуляцию.
-- Хорошо
local function foo()
  if condition then
    bar()
  end
end
  • Ставьте пробелы вокруг операторов.
-- Плохо
local x=1+2

-- Хорошо
local x = 1 + 2
  • Ставьте пробел после запятой.
-- Плохо
local tbl = {1,2,3}

-- Хорошо
local tbl = { 1, 2, 3 }
  • Не ставьте пробелы внутри скобок вызова функции.
-- Плохо
print( 'hello' )

-- Хорошо
print('hello')
  • Оставляйте одну пустую строку между функциями.

  • Максимальная длина строки — 130 символов.

  • Ставьте запятую после каждого элемента таблицы, включая последний (trailing comma).
-- Хорошо
local tbl = {
  name = 'John',
  age = 27,
}
  • В однострочных таблицах trailing comma не нужна.
local point = { x = 1, y = 2 }
  • Не используйте точку с запятой ; для разделения выражений.
-- Плохо
local a = 1; local b = 2;

-- Хорошо
local a = 1
local b = 2
  • Используйте tonumber() для явного приведения к числу.
model.id = tonumber(data.id)
  • Используйте tostring() для явного приведения к строке.
model.first_name = tostring(data.first_name)
  • Не полагайтесь на неявное приведение типов.
-- Плохо
local result = '5' + 3

-- Хорошо
local result = tonumber('5') + 3
  • camelCase — для переменных и функций.
local userId = 1
local function getUserById(id) end
  • В отдельных случаях допустимо использование snake_case для методов (например, Errors:field_missing), когда это обусловлено стилем API объекта.

  • PascalCase — для конструкторов и классов.

local function User(data) end
local function Errors() end
  • UPPER_CASE — для констант и значений перечислений.
local roles = {
  USER = 1,
  GUIDE = 2,
  ADMIN = 3,
}
  • _snake_case — для приватных методов и внутренних переменных модуля.
function MyClass:_internal_method()
  -- ...
end
  • Для булевых функций используйте префиксы is / has.
-- Плохо
local function valid(data)
  return data.id ~= nil
end

-- Хорошо
local function isValid(data)
  return data.id ~= nil
end

local function hasErrors(self)
  return self.count > 0
end
  • Аргументы функций допустимо писать в snake_case, но предпочтительнее camelCase.
-- Допустимо
local function createUser(avatar_url)
  -- ...
end

-- Лучше
local function createUser(avatarUrl)
  -- ...
end
  • Поля объектов хранилища (Tarantool spaces) пишутся в snake_case.
-- Поля из БД
model.first_name = tostring(data.first_name)
model.avatar_url = data.avatar_url
  • Имена файлов: PascalCase для моделей и сервисов (User.lua, Errors.lua, UserService.lua), snake_case для утилит (find.lua, merge.lua).

  • Неиспользуемые переменные именуйте _.

for _, val in pairs(tbl) do
  • Избегайте однобуквенных имён, кроме итераторов (i, k, v, _).
  • Конструкторы именуются в PascalCase и возвращают model, errors.
--- Конструктор модели User.
-- @tparam table data Сырые поля (из запроса или из БД)
-- @tparam ?table opts Опции { init = true } - инициализация всех полей
-- @treturn[1] table model
-- @treturn[2] table errs
local function User(data, opts)
  local init = opts and opts.init
  local errors = Errors:new({ space = 'users' })
  local model = {}

  -- Валидация и заполнение полей
  if tonumber(data.id) then
    model.id = tonumber(data.id)
  else
    errors:field_missing('id')
  end

  if errors:has_errors() then
    return nil, errors:get_compact()
  end

  return model, nil
end

return User
  • Для OOP-конструкторов используйте setmetatable и метод new.
local MyClass = {}
MyClass.__index = MyClass

function MyClass:new(params)
  local obj = {}
  obj.field = params.field
  setmetatable(obj, self)
  return obj
end
  • Каждый файл — один модуль. Модуль заканчивается одним return.
-- Утилита — возвращает функцию
local function find(tbl, value)
  -- ...
end

return find
-- Сервис — возвращает таблицу с методами
local service = {}

function service.create(data) end
function service.read(id) end
function service.update(fields, where) end
function service.delete(id) end

return service
-- Перечисление — возвращает таблицу констант
return {
  USER = 1,
  GUIDE = 2,
  ADMIN = 3,
}
  • require вызовы группируйте в начале файла.
  • Сортируйте порядок require-ов от системных/встроенных модулей к модулям приложения.
local log = require('log')
local datetime = require('datetime')
local sql = require('src.libs.sql')
local User = require('src.models.User')
local Errors = require('src.models.Errors')
local roles = require('src.enums.roles')
  • Именуйте переменную require по имени модуля. Не переименовывайте произвольно.

Обоснование: Код читается проще, если не нужно возвращаться к началу файла, чтобы проверить, как именно назван модуль.

-- Плохо
local skt = require('socket')
local j = require('json')

-- Хорошо
local socket = require('socket')
local json = require('json')
  • Подключение модуля через require не должно вызывать побочных эффектов (запись в БД, вывод в консоль, изменение глобального состояния). Единственное допустимое действие — загрузка зависимостей и возврат таблицы модуля.

  • Не используйте module(). Всегда используйте паттерн с return.

  • Используйте __index для наследования.
local Errors = {}
Errors.__index = Errors
  • Используйте __serialize для контроля сериализации (Tarantool). Например, пустая таблица по умолчанию сериализуется как массив [] — чтобы она стала объектом {}, задайте __serialize = 'map'.
setmetatable(result, { __serialize = 'map' })
  • Используйте __tostring для отладочного вывода объектов.

  • Не злоупотребляйте метаметодами. Если задачу можно решить без них — решайте без них.

  • Файл организуется в следующем порядке:
-- 1. Подключение модулей (require)
local log = require('log')
local sql = require('src.libs.sql')
local User = require('src.models.User')

-- 2. Локальные переменные и константы
local MAX_RETRIES = 3

-- 3. Локальные функции (вспомогательные)
local function helper()
  -- ...
end

-- 4. Основная логика / определение модуля
local service = {}

function service.create(data)
  -- ...
end

-- 5. Возврат модуля
return service
  • Правила оформления комментариев и документации описаны в секции Комментарии.
  • Функции, которые могут завершиться с ожидаемой ошибкой (I/O, валидация, запросы к БД), возвращают nil, err.
local user, err = service.read(userId)
if err then
  return nil, err
end
  • При ошибке API (неправильное использование функции) используйте error() или assert().
function service.read(id)
  assert(type(id) == 'number', 'id must be a number')
  -- ...
end
  • Не возвращайте больше двух значений. Если нужно вернуть дополнительные данные — оберните их в таблицу.
-- Плохо
return result, err, details, code

-- Хорошо
return result, { message = err, details = details, code = code }
  • При проверке ошибки сначала проверяйте err / nil, затем продолжайте работу.
-- Плохо
local user, err = service.read(id)
if user then
  -- ...
end

-- Хорошо
local user, err = service.read(id)
if err then
  log.error(err)
  return nil, err
end

-- работа с user
  • Для накопления ошибок валидации используйте объект ошибок.
local errors = Errors:new({ space = 'users' })

if not data.id then
  errors:field_missing('id')
end

if not data.name then
  errors:field_missing('name')
end

if errors:has_errors() then
  return nil, errors:get_compact()
end
  • Код должен проходить проверку luacheck.

  • Настройки проекта описаны в .luacheckrc.

  • Основные параметры:

Параметр Значение
Стандарт min
Макс. длина строки 130 символов
Допустимые глобальные box, _TARANTOOL, p
  • Предупреждения luacheck, которые допустимо игнорировать (настроены в .luacheckrc):
Код Описание
411 Переопределение локальной переменной
412 Переопределение аргумента
413 Переопределение переменной цикла
421–423 Затенение (shadowing) локальных переменных
431–433 Затенение upvalue
581 Использование not (x == y)
  • Если luacheck выдаёт ложное срабатывание — используйте директивы в комментариях (см. Комментарии).
  • Тестовые файлы размещаются в директории tests/.

  • Именование тестовых файлов: *_test.lua.

  • Используйте luatest в качестве фреймворка для тестирования.

local t = require('luatest')
local g = t.group('group_name')

g.before_all(function()
  -- подготовка
end)

g.test_example = function()
  local result = service.read(1)
  t.assert_is_not(result, nil)
  t.assert_equals(result.id, 1)
end
  • Каждый тест должен быть независимым от других.

  • Тестируйте граничные случаи и ошибки, а не только "happy path".

  • Избегайте создания лишних замыканий и таблиц в циклах.
-- Плохо: новое замыкание на каждой итерации
for i = 1, #items do
  items[i]:on('event', function() handle(items[i]) end)
end

-- Хорошо: один общий обработчик, если событие передаёт источник
local function onEvent(item)
  handle(item)
end

for i = 1, #items do
  items[i]:on('event', onEvent)
end
  • Если API не передаёт контекст в обработчик, замыкание на каждый элемент неизбежно. В этом случае выносите из цикла всё остальное — например, таблицы опций.
-- Плохо: одинаковая таблица создаётся на каждой итерации
for i = 1, #rows do
  send(rows[i], { timeout = 3 })
end

-- Хорошо: таблица создаётся один раз
local opts = { timeout = 3 }
for i = 1, #rows do
  send(rows[i], opts)
end
  • Используйте table.new(narr, nrec) для предаллокации таблиц, если известен примерный размер. В Tarantool функция доступна сразу, в чистом LuaJIT — через require('table.new').

Releases

Packages

Contributors