fix(gcal): add LuaCATS annotations and resolve type errors

Problem: gcal.lua had ~10 LuaLS errors from untyped credential and
token tables, string|osdate casts, and untyped _gcal_event_id
field access.

Solution: add pending.GcalCredentials and pending.GcalTokens class
definitions, annotate all local functions with @param/@return, add
--[[@as string]] casts on os.date returns, and fix _gcal_event_id
access to use bracket notation with casts.
This commit is contained in:
Barrett Ruth 2026-02-24 18:29:46 -05:00 committed by Barrett Ruth
parent ce8decc2b0
commit 00ea92ce33

View file

@ -8,20 +8,36 @@ local TOKEN_URL = 'https://oauth2.googleapis.com/token'
local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
local SCOPE = 'https://www.googleapis.com/auth/calendar'
---@class pending.GcalCredentials
---@field client_id string
---@field client_secret string
---@field redirect_uris? string[]
---@class pending.GcalTokens
---@field access_token string
---@field refresh_token string
---@field expires_in? integer
---@field obtained_at? integer
---@return table<string, any>
local function gcal_config()
local cfg = config.get()
return cfg.gcal or {}
end
---@return string
local function token_path()
return vim.fn.stdpath('data') .. '/pending/gcal_tokens.json'
end
---@return string
local function credentials_path()
local gc = gcal_config()
return gc.credentials_path or (vim.fn.stdpath('data') .. '/pending/gcal_credentials.json')
end
---@param path string
---@return table?
local function load_json_file(path)
local f = io.open(path, 'r')
if not f then
@ -39,6 +55,9 @@ local function load_json_file(path)
return decoded
end
---@param path string
---@param data table
---@return boolean
local function save_json_file(path, data)
local dir = vim.fn.fnamemodify(path, ':h')
if vim.fn.isdirectory(dir) == 0 then
@ -54,31 +73,43 @@ local function save_json_file(path, data)
return true
end
---@return pending.GcalCredentials?
local function load_credentials()
local creds = load_json_file(credentials_path())
if not creds then
return nil
end
if creds.installed then
return creds.installed
return creds.installed --[[@as pending.GcalCredentials]]
end
return creds
return creds --[[@as pending.GcalCredentials]]
end
---@return pending.GcalTokens?
local function load_tokens()
return load_json_file(token_path())
return load_json_file(token_path()) --[[@as pending.GcalTokens?]]
end
---@param tokens pending.GcalTokens
---@return boolean
local function save_tokens(tokens)
return save_json_file(token_path(), tokens)
end
---@param str string
---@return string
local function url_encode(str)
return str:gsub('([^%w%-%.%_%~])', function(c)
return string.format('%%%02X', string.byte(c))
end)
end
---@param method string
---@param url string
---@param headers? string[]
---@param body? string
---@return table? result
---@return string? err
local function curl_request(method, url, headers, body)
local args = { 'curl', '-s', '-X', method }
for _, h in ipairs(headers or {}) do
@ -107,6 +138,8 @@ local function curl_request(method, url, headers, body)
return decoded, nil
end
---@param access_token string
---@return string[]
local function auth_headers(access_token)
return {
'Authorization: Bearer ' .. access_token,
@ -114,6 +147,9 @@ local function auth_headers(access_token)
}
end
---@param creds pending.GcalCredentials
---@param tokens pending.GcalTokens
---@return pending.GcalTokens?
local function refresh_access_token(creds, tokens)
local body = 'client_id='
.. url_encode(creds.client_id)
@ -142,13 +178,14 @@ local function refresh_access_token(creds, tokens)
if not ok or not decoded.access_token then
return nil
end
tokens.access_token = decoded.access_token
tokens.expires_in = decoded.expires_in
tokens.access_token = decoded.access_token --[[@as string]]
tokens.expires_in = decoded.expires_in --[[@as integer?]]
tokens.obtained_at = os.time()
save_tokens(tokens)
return tokens
end
---@return string?
local function get_access_token()
local creds = load_credentials()
if not creds then
@ -260,6 +297,10 @@ function M.authorize()
end)
end
---@param creds pending.GcalCredentials
---@param code string
---@param code_verifier string
---@param port integer
function M._exchange_code(creds, code, code_verifier, port)
local body = 'client_id='
.. url_encode(creds.client_id)
@ -303,6 +344,9 @@ function M._exchange_code(creds, code, code_verifier, port)
vim.notify('pending.nvim: Google Calendar authorized successfully.')
end
---@param access_token string
---@return string? calendar_id
---@return string? err
local function find_or_create_calendar(access_token)
local gc = gcal_config()
local cal_name = gc.calendar or 'Pendings'
@ -329,18 +373,25 @@ local function find_or_create_calendar(access_token)
return created and created.id, nil
end
---@param date_str string
---@return string
local function next_day(date_str)
local y, m, d = date_str:match('^(%d%d%d%d)-(%d%d)-(%d%d)$')
local t = os.time({ year = tonumber(y) or 0, month = tonumber(m) or 0, day = tonumber(d) or 0 })
+ 86400
return os.date('%Y-%m-%d', t)
return os.date('%Y-%m-%d', t) --[[@as string]]
end
---@param access_token string
---@param calendar_id string
---@param task pending.Task
---@return string? event_id
---@return string? err
local function create_event(access_token, calendar_id, task)
local event = {
summary = task.description,
start = { date = task.due },
['end'] = { date = next_day(task.due) },
['end'] = { date = next_day(task.due or '') },
transparency = 'transparent',
extendedProperties = {
private = { taskId = tostring(task.id) },
@ -358,11 +409,16 @@ local function create_event(access_token, calendar_id, task)
return data and data.id, nil
end
---@param access_token string
---@param calendar_id string
---@param event_id string
---@param task pending.Task
---@return string? err
local function update_event(access_token, calendar_id, event_id, task)
local event = {
summary = task.description,
start = { date = task.due },
['end'] = { date = next_day(task.due) },
['end'] = { date = next_day(task.due or '') },
}
local _, err = curl_request(
'PATCH',
@ -373,6 +429,10 @@ local function update_event(access_token, calendar_id, event_id, task)
return err
end
---@param access_token string
---@param calendar_id string
---@param event_id string
---@return string? err
local function delete_event(access_token, calendar_id, event_id)
local _, err = curl_request(
'DELETE',
@ -399,25 +459,25 @@ function M.sync()
for _, task in ipairs(tasks) do
local extra = task._extra or {}
local event_id = extra._gcal_event_id
local event_id = extra['_gcal_event_id'] --[[@as string?]]
local should_delete = event_id
local should_delete = event_id ~= nil
and (
task.status == 'done'
or task.status == 'deleted'
or (task.status == 'pending' and not task.due)
)
if should_delete then
local del_err = delete_event(access_token, calendar_id, event_id)
if should_delete and event_id then
local del_err = delete_event(access_token, calendar_id, event_id) --[[@as string]]
if not del_err then
extra._gcal_event_id = nil
extra['_gcal_event_id'] = nil
if next(extra) == nil then
task._extra = nil
else
task._extra = extra
end
task.modified = tostring(os.date('!%Y-%m-%dT%H:%M:%SZ'))
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
deleted = deleted + 1
end
elseif task.status == 'pending' and task.due then
@ -432,8 +492,8 @@ function M.sync()
if not task._extra then
task._extra = {}
end
task._extra._gcal_event_id = new_id
task.modified = tostring(os.date('!%Y-%m-%dT%H:%M:%SZ'))
task._extra['_gcal_event_id'] = new_id
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
created = created + 1
end
end