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 68dbea7d52
commit fc45ca3fcd

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