-- SCL Diagnostics - Linter functionality for the LSP server local M = {} -- Get script path for loading parser (same pattern as main.lua) local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or "" if script_path == "" then script_path = "." end if script_path:sub(1, 1) ~= "/" then local cwd = io.popen("pwd") if cwd then script_path = cwd:read("*l") .. "/" .. script_path cwd:close() end end package.path = package.path .. ";" .. script_path .. "/?.lua" local parser = dofile(script_path .. "/parser.lua") local DiagnosticSeverity = { Error = 1, Warning = 2, Information = 3, Hint = 4, } local function position_to_lsp(line, character) return { line = line, character = character, } end local function range_to_lsp(start_line, start_char, end_line, end_char) return { start = position_to_lsp(start_line, start_char), ["end"] = position_to_lsp(end_line, end_char), } end function M.validate_diagnostics(content, workspace_types, root_dir) local diagnostics = {} local lines = {} for line in content:gmatch("([^\n]*)\n") do table.insert(lines, line) end local variables, _ = parser.extract_variables(content) local types = parser.extract_types(content) local functions = parser.extract_functions(content) local all_types = {} for k, v in pairs(types) do all_types[k] = v end for k, v in pairs(workspace_types or {}) do all_types[k] = v end local in_var_section = false local var_section_start = nil for line_idx, line in ipairs(lines) do local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "") local line_num = line_idx - 1 if trimmed:match("^VAR[_A-Z]*%s*$") or trimmed:match("^VAR_INPUT%s*$") or trimmed:match("^VAR_OUTPUT%s*$") or trimmed:match("^VAR_IN_OUT%s*$") or trimmed:match("^VAR_TEMP%s*$") or trimmed:match("^VAR[_ ]*CONSTANT%s*$") or trimmed:match("^VAR[_ ]*RETAIN%s*$") or trimmed:match("^VAR[_ ]*NON_RETAIN%s*$") then in_var_section = true var_section_start = line_num elseif trimmed:match("^END_VAR%s*$") then in_var_section = false var_section_start = nil end if not in_var_section then local hash_vars = {} for var in line:gmatch("#[%w_]+") do local var_name = var:sub(2) hash_vars[var_name] = true end for var_name, _ in pairs(hash_vars) do if not variables[var_name] and not all_types[var_name] then local var_start = line:find("#" .. var_name, 1, true) if var_start then table.insert(diagnostics, { range = range_to_lsp(line_num, var_start - 1, line_num, var_start + #var_name - 1), severity = DiagnosticSeverity.Error, message = string.format("Undefined variable: %s", var_name), code = "SCL001", source = "scl_lsp", }) end end end end if in_var_section and (trimmed:match("^[%w_]+%s*%b{}%s*:") or trimmed:match("^[%w_]+%s*:")) then local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:") local var_start = line:find(var_name, 1, true) if var_name and not var_name:match("^%d") then local has_hash = line:match("#" .. var_name) if has_hash then table.insert(diagnostics, { range = range_to_lsp(line_num, 0, line_num, #line), severity = DiagnosticSeverity.Error, message = "Variable declarations should NOT have '#' prefix in VAR section", code = "SCL002", source = "scl_lsp", }) end local type_part = nil local brace_depth = 0 local comment_depth = 0 local last_colon = nil for i = 1, #line do local c = line:sub(i, i) local next_c = line:sub(i+1, i+1) if c == "(" and next_c == "*" then comment_depth = comment_depth + 1 elseif c == "*" and next_c == ")" and comment_depth > 0 then comment_depth = comment_depth - 1 elseif c == "{" and comment_depth == 0 then brace_depth = brace_depth + 1 elseif c == "}" and comment_depth == 0 then brace_depth = brace_depth - 1 elseif c == ":" and brace_depth == 0 and comment_depth == 0 then last_colon = i end end if last_colon then type_part = line:sub(last_colon + 1) end if type_part then type_part = type_part:gsub("%s*%(%*.-%*%)", "") -- Remove block comments (* ... *) with leading whitespace type_part = type_part:gsub("%s*:=.*$", "") type_part = type_part:gsub("%s*;.*$", "") -- Remove trailing semicolon type_part = type_part:gsub("%s*$", "") type_part = type_part:gsub("^%s+", "") local base_type = type_part:match("^([%w_]+)") if type_part:match("%s+OF%s+") then base_type = type_part:match(".*%s+OF%s+([%w_]+)") end local upper_type = base_type and base_type:upper() or nil if base_type and not all_types[base_type] and upper_type ~= "BOOL" and upper_type ~= "INT" and upper_type ~= "DINT" and upper_type ~= "REAL" and upper_type ~= "LREAL" and upper_type ~= "BYTE" and upper_type ~= "WORD" and upper_type ~= "DWORD" and upper_type ~= "LWORD" and upper_type ~= "CHAR" and upper_type ~= "STRING" and upper_type ~= "WCHAR" and upper_type ~= "WSTRING" and upper_type ~= "TIME" and upper_type ~= "DATE" and upper_type ~= "TIME_OF_DAY" and upper_type ~= "DATE_AND_TIME" and upper_type ~= "S5TIME" and upper_type ~= "LTIME" and upper_type ~= "IEC_TIMER" and upper_type ~= "TON_TIME" and upper_type ~= "TOF_TIME" and upper_type ~= "TONR_TIME" and upper_type ~= "TP_TIME" then table.insert(diagnostics, { range = range_to_lsp(line_num, 0, line_num, #line), severity = DiagnosticSeverity.Warning, message = string.format("Unknown data type: %s", base_type), code = "SCL003", source = "scl_lsp", }) end end end end if trimmed:match("^%s*//") or trimmed:match("^%s*%(%*") then -- Skip comments else for assignment in line:gmatch("#[%w_]+%s*:=") do local var_name = assignment:match("#([%w_]+)") if var_name and in_var_section then local assign_start = line:find("#" .. var_name, 1, true) table.insert(diagnostics, { range = range_to_lsp(line_num, assign_start - 1, line_num, assign_start + #var_name), severity = DiagnosticSeverity.Error, message = "Cannot use '#' prefix for variables in VAR section", code = "SCL004", source = "scl_lsp", }) end end end end for func_idx, func in ipairs(functions) do local func_content = "" for i = func.start + 1, func.final - 1 do if lines[i + 1] then func_content = func_content .. lines[i + 1] .. "\n" end end local func_vars, _ = parser.extract_variables(func_content) local all_vars = {} for k, v in pairs(variables) do all_vars[k] = v end for k, v in pairs(func_vars) do all_vars[k] = v end end return diagnostics end return M