From d94cb62e0ba795b24776ac6a5e8b85bc06fac696 Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Thu, 19 Feb 2026 12:01:33 +0100 Subject: [PATCH] fix(lsp): add more robust validation in semantic tokens - Wrap token generation in pcall to catch any errors - Add validation for all token parameters (line, startChar, length) - Validate deltaStart is non-negative - Return empty tokens if any error occurs - This should prevent the 'Invalid line: out of range' errors --- src/main.lua | 147 +++++++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/src/main.lua b/src/main.lua index f689aa9..6abdde9 100644 --- a/src/main.lua +++ b/src/main.lua @@ -563,92 +563,91 @@ function handlers.textDocument_semanticTokensFull(params) return { data = {} } end + -- Protected token collection local tokens = {} - local lines = {} - for line in doc.content:gmatch("([^\n]*)\n") do - table.insert(lines, line) - end - - local total_lines = #lines - - local function add_token(line, startChar, length, tokenType) - -- Validate line number is within bounds - if line < 0 or line >= total_lines then - return + local ok, err = pcall(function() + local lines = {} + for line in doc.content:gmatch("([^\n]*)\n") do + table.insert(lines, line) end - local deltaLine = line - local deltaStart = startChar - if #tokens >= 4 then - local prevLine = tokens[#tokens - 3] - local prevStart = tokens[#tokens - 2] - if line == prevLine then - deltaLine = 0 - deltaStart = startChar - prevStart - else - deltaLine = line - prevLine + + local total_lines = #lines + + local function add_token(line, startChar, length) + -- Validate all parameters + if not line or not startChar or not length then + return end + if line < 0 or line >= total_lines then + return + end + if startChar < 0 or length <= 0 then + return + end + if startChar + length > 10000 then + return + end + + local deltaLine = line + local deltaStart = startChar + if #tokens >= 4 then + local prevLine = tokens[#tokens - 3] + local prevStart = tokens[#tokens - 2] + if line == prevLine then + deltaLine = 0 + deltaStart = startChar - prevStart + else + deltaLine = line - prevLine + end + -- Validate deltaStart is non-negative + if deltaStart < 0 then + return + end + end + table.insert(tokens, deltaLine) + table.insert(tokens, deltaStart) + table.insert(tokens, length) + table.insert(tokens, 0) + table.insert(tokens, 0) end - table.insert(tokens, deltaLine) - table.insert(tokens, deltaStart) - table.insert(tokens, length) - table.insert(tokens, 0) - table.insert(tokens, 0) - end - for i, line in ipairs(lines) do - local lineNum = i - 1 - local trimmed = line:gsub("^%s+", "") + for i, line in ipairs(lines) do + local lineNum = i - 1 + local trimmed = line:gsub("^%s+", "") - if doc.variables then - for var_name, info in pairs(doc.variables) do - -- Validate variable line number is within bounds - if info.line and info.line >= 0 and info.line < total_lines and info.line == lineNum then - local startChar = line:find(var_name, 1, true) - if startChar then - add_token(lineNum, startChar - 1, #var_name, "variable") + if doc.variables then + for var_name, info in pairs(doc.variables) do + if info and info.line and info.line >= 0 and info.line < total_lines and info.line == lineNum then + local startChar = line:find(var_name, 1, true) + if startChar then + add_token(lineNum, startChar - 1, #var_name) + end end end end - end - local keywords = { - "IF", - "THEN", - "ELSIF", - "ELSE", - "END_IF", - "FOR", - "TO", - "BY", - "DO", - "END_FOR", - "WHILE", - "END_WHILE", - "REPEAT", - "UNTIL", - "END_REPEAT", - "CASE", - "OF", - "END_CASE", - "VAR_INPUT", - "VAR_OUTPUT", - "VAR_IN_OUT", - "VAR_TEMP", - "VAR", - "END_VAR", - "BEGIN", - "EXIT", - "CONTINUE", - "RETURN", - "TRUE", - "FALSE", - } - for _, kw in ipairs(keywords) do - local startPos = line:find("%f[%a]" .. kw .. "%f[%A]") - if startPos then - add_token(lineNum, startPos - 1, #kw, "keyword") + local keywords = { + "IF", "THEN", "ELSIF", "ELSE", "END_IF", + "FOR", "TO", "BY", "DO", "END_FOR", + "WHILE", "END_WHILE", + "REPEAT", "UNTIL", "END_REPEAT", + "CASE", "OF", "END_CASE", + "VAR_INPUT", "VAR_OUTPUT", "VAR_IN_OUT", "VAR_TEMP", "VAR", "END_VAR", + "BEGIN", "EXIT", "CONTINUE", "RETURN", + "TRUE", "FALSE", + } + for _, kw in ipairs(keywords) do + local startPos = line:find("%f[%a]" .. kw .. "%f[%A]") + if startPos then + add_token(lineNum, startPos - 1, #kw) + end end end + end) + + -- If any error occurred, return empty tokens + if not ok then + return { data = {} } end return { data = tokens }