pending.nvim/lua/pending/parse.lua
Barrett Ruth 2b73ab1cd0 fix: resolve remaining LuaLS type errors
Problem: CI lua-typecheck-action reported three categories of errors:
1. parse.lua - multi-assignment of tonumber() results left y/m/d typed
   as number? rather than integer, failing os.time()'s field types
2. gcal.lua - url_encode returned str:gsub() which yields string+integer
   but the annotation declared @return string (redundant-return-value)
3. gcal.lua - calendar_id typed string? from find_or_create_calendar was
   passed to functions expecting string; the existing `if err` guard did
   not narrow the type for LuaLS

Solution: replace the y/m/d multi-assignment with yn/mn/dn locals whose
types resolve cleanly to integer; wrap the gsub return in parentheses to
discard the count; add `or not calendar_id` to the error guard so LuaLS
narrows calendar_id to string for the rest of the scope.
2026-02-24 18:50:28 -05:00

168 lines
3.9 KiB
Lua

local config = require('pending.config')
---@class pending.parse
local M = {}
---@param s string
---@return boolean
local function is_valid_date(s)
local y, m, d = s:match('^(%d%d%d%d)-(%d%d)-(%d%d)$')
if not y then
return false
end
local yn = tonumber(y) --[[@as integer]]
local mn = tonumber(m) --[[@as integer]]
local dn = tonumber(d) --[[@as integer]]
if mn < 1 or mn > 12 then
return false
end
if dn < 1 or dn > 31 then
return false
end
local t = os.time({ year = yn, month = mn, day = dn })
local check = os.date('*t', t) --[[@as osdate]]
return check.year == yn and check.month == mn and check.day == dn
end
---@return string
local function date_key()
return config.get().date_syntax or 'due'
end
local weekday_map = {
sun = 1,
mon = 2,
tue = 3,
wed = 4,
thu = 5,
fri = 6,
sat = 7,
}
---@param text string
---@return string|nil
function M.resolve_date(text)
local lower = text:lower()
local today = os.date('*t') --[[@as osdate]]
if lower == 'today' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
end
if lower == 'tomorrow' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + 1 })
) --[[@as string]]
end
local n = lower:match('^%+(%d+)d$')
if n then
return os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month,
day = today.day + (
tonumber(n) --[[@as integer]]
),
})
) --[[@as string]]
end
local target_wday = weekday_map[lower]
if target_wday then
local current_wday = today.wday
local delta = (target_wday - current_wday) % 7
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + delta })
) --[[@as string]]
end
return nil
end
---@param text string
---@return string description
---@return { due?: string, cat?: string } metadata
function M.body(text)
local tokens = {}
for token in text:gmatch('%S+') do
table.insert(tokens, token)
end
local metadata = {}
local i = #tokens
local dk = date_key()
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d)$'
local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
while i >= 1 do
local token = tokens[i]
local due_val = token:match(date_pattern_strict)
if due_val then
if metadata.due then
break
end
if not is_valid_date(due_val) then
break
end
metadata.due = due_val
i = i - 1
else
local raw_val = token:match(date_pattern_any)
if raw_val then
if metadata.due then
break
end
local resolved = M.resolve_date(raw_val)
if not resolved then
break
end
metadata.due = resolved
i = i - 1
else
local cat_val = token:match('^cat:(%S+)$')
if cat_val then
if metadata.cat then
break
end
metadata.cat = cat_val
i = i - 1
else
break
end
end
end
end
local desc_tokens = {}
for j = 1, i do
table.insert(desc_tokens, tokens[j])
end
local description = table.concat(desc_tokens, ' ')
return description, metadata
end
---@param text string
---@return string description
---@return { due?: string, cat?: string } metadata
function M.command_add(text)
local cat_prefix = text:match('^(%S.-):%s')
if cat_prefix then
local first_char = cat_prefix:sub(1, 1)
if first_char == first_char:upper() and first_char ~= first_char:lower() then
local rest = text:sub(#cat_prefix + 2):match('^%s*(.+)$')
if rest then
local desc, meta = M.body(rest)
meta.cat = meta.cat or cat_prefix
return desc, meta
end
end
end
return M.body(text)
end
return M