From fc45ca3fcd4af549017002bc25275507c0a9856c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 24 Feb 2026 18:29:46 -0500 Subject: [PATCH] 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. --- lua/pending/sync/gcal.lua | 92 ++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index a5f57f3..f3c84e2 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -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 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