local util = require 'utility' local scope = require 'workspace.scope' local guide = require 'parser.guide' local config = require 'config' ---@class vm local vm = require 'vm.vm' ---@type table local allGlobals = {} ---@type table> local globalSubs = util.multiTable(2) ---@class parser.object ---@field package _globalBase parser.object ---@field package _globalBaseMap table ---@field global vm.global ---@class vm.global.link ---@field sets parser.object[] ---@field gets parser.object[] ---@class vm.global ---@field links table ---@field setsCache? table ---@field cate vm.global.cate local mt = {} mt.__index = mt mt.type = 'global' mt.name = '' ---@param uri uri ---@param source parser.object function mt:addSet(uri, source) local link = self.links[uri] link.sets[#link.sets+1] = source self.setsCache = nil end ---@param uri uri ---@param source parser.object function mt:addGet(uri, source) local link = self.links[uri] link.gets[#link.gets+1] = source end ---@param suri uri ---@return parser.object[] function mt:getSets(suri) if not self.setsCache then self.setsCache = {} end local scp = scope.getScope(suri) local cacheUri = scp.uri or '' if self.setsCache[cacheUri] then return self.setsCache[cacheUri] end local clock = os.clock() self.setsCache[cacheUri] = {} local cache = self.setsCache[cacheUri] for uri, link in pairs(self.links) do if link.sets then if scp:isVisible(uri) then for _, source in ipairs(link.sets) do cache[#cache+1] = source end end end end local cost = os.clock() - clock if cost > 0.1 then log.warn('global-manager getSets costs', cost, self.name) end return cache end ---@return parser.object[] function mt:getAllSets() if not self.setsCache then self.setsCache = {} end local cache = self.setsCache['*'] if cache then return cache end cache = {} self.setsCache['*'] = cache for _, link in pairs(self.links) do if link.sets then for _, source in ipairs(link.sets) do cache[#cache+1] = source end end end return cache end ---@param uri uri function mt:dropUri(uri) self.links[uri] = nil self.setsCache = nil end ---@return string function mt:getName() return self.name end ---@return string function mt:getCodeName() return (self.name:gsub(vm.ID_SPLITE, '.')) end ---@return string function mt:asKeyName() return self.cate .. '|' .. self.name end ---@return string function mt:getKeyName() return self.name:match('[^' .. vm.ID_SPLITE .. ']+$') end ---@return string? function mt:getFieldName() return self.name:match(vm.ID_SPLITE .. '(.-)$') end ---@return boolean function mt:isAlive() return next(self.links) ~= nil end ---@param uri uri ---@return parser.object? function mt:getParentBase(uri) local parentID = self.name:match('^(.-)' .. vm.ID_SPLITE) if not parentID then return nil end local parentName = self.cate .. '|' .. parentID local global = allGlobals[parentName] if not global then return nil end local link = global.links[uri] if not link then return nil end local luckyBoy = link.sets[1] or link.gets[1] if not luckyBoy then return nil end return vm.getGlobalBase(luckyBoy) end ---@param cate vm.global.cate ---@return vm.global local function createGlobal(name, cate) return setmetatable({ name = name, cate = cate, links = util.multiTable(2, function () return { sets = {}, gets = {}, } end), }, mt) end ---@class parser.object ---@field package _globalNode vm.global|false ---@field package _enums? parser.object[] local compileObject local compilerGlobalSwitch = util.switch() : case 'local' : call(function (source) if source.special ~= '_G' then return end if source.ref then for _, ref in ipairs(source.ref) do compileObject(ref) end end end) : case 'getlocal' : call(function (source) if source.special ~= '_G' then return end if not source.next then return end compileObject(source.next) end) : case 'setglobal' : call(function (source) local uri = guide.getUri(source) local name = guide.getKeyName(source) if not name then return end local global = vm.declareGlobal('variable', name, uri) global:addSet(uri, source) source._globalNode = global end) : case 'getglobal' : call(function (source) local uri = guide.getUri(source) local name = guide.getKeyName(source) if not name then return end local global = vm.declareGlobal('variable', name, uri) global:addGet(uri, source) source._globalNode = global local nxt = source.next if nxt then compileObject(nxt) end end) : case 'setfield' : case 'setmethod' : case 'setindex' ---@param source parser.object : call(function (source) local name local keyName = guide.getKeyName(source) if not keyName then return end if source.node._globalNode then local parentName = source.node._globalNode:getName() if parentName == '_G' then name = keyName else name = ('%s%s%s'):format(parentName, vm.ID_SPLITE, keyName) end elseif source.node.special == '_G' then name = keyName end if not name then return end local uri = guide.getUri(source) local global = vm.declareGlobal('variable', name, uri) global:addSet(uri, source) source._globalNode = global end) : case 'getfield' : case 'getmethod' : case 'getindex' ---@param source parser.object : call(function (source) local name local keyName = guide.getKeyName(source) if not keyName then return end if source.node._globalNode then local parentName = source.node._globalNode:getName() if parentName == '_G' then name = keyName else name = ('%s%s%s'):format(parentName, vm.ID_SPLITE, keyName) end elseif source.node.special == '_G' then name = keyName end local uri = guide.getUri(source) local global = vm.declareGlobal('variable', name, uri) global:addGet(uri, source) source._globalNode = global local nxt = source.next if nxt then compileObject(nxt) end end) : case 'call' : call(function (source) if source.node.special == 'rawset' or source.node.special == 'rawget' then if not source.args then return end local g = source.args[1] local key = source.args[2] if g and key and g.special == '_G' then local name = guide.getKeyName(key) if name then local uri = guide.getUri(source) local global = vm.declareGlobal('variable', name, uri) if source.node.special == 'rawset' then global:addSet(uri, source) source.value = source.args[3] else global:addGet(uri, source) end source._globalNode = global local nxt = source.next if nxt then compileObject(nxt) end end end end end) : case 'doc.class' ---@param source parser.object : call(function (source) local uri = guide.getUri(source) local name = guide.getKeyName(source) if not name then return end local class = vm.declareGlobal('type', name, uri) class:addSet(uri, source) source._globalNode = class if source.signs then local sign = vm.createSign() vm.setSign(source, sign) for _, obj in ipairs(source.signs) do sign:addSign(vm.compileNode(obj)) end if source.extends then for _, ext in ipairs(source.extends) do if ext.type == 'doc.type.table' then vm.setGeneric(ext, vm.createGeneric(ext, sign)) end end end end end) : case 'doc.alias' : call(function (source) local uri = guide.getUri(source) local name = guide.getKeyName(source) if not name then return end local alias = vm.declareGlobal('type', name, uri) alias:addSet(uri, source) source._globalNode = alias if source.signs then source._sign = vm.createSign() for _, sign in ipairs(source.signs) do source._sign:addSign(vm.compileNode(sign)) end source.extends._generic = vm.createGeneric(source.extends, source._sign) end end) : case 'doc.enum' : call(function (source) local uri = guide.getUri(source) local name = guide.getKeyName(source) if not name then return end local enum = vm.declareGlobal('type', name, uri) enum:addSet(uri, source) source._globalNode = enum local tbl = source.bindSource if not tbl then return end source._enums = {} if vm.docHasAttr(source, 'key') then for _, field in ipairs(tbl) do if field.type == 'tablefield' then source._enums[#source._enums+1] = { type = 'doc.type.string', start = field.field.start, finish = field.field.finish, [1] = field.field[1], } elseif field.type == 'tableindex' then if field.index then source._enums[#source._enums+1] = { type = 'doc.type.string', start = field.index.start, finish = field.index.finish, [1] = field.index[1], } end end end else for _, field in ipairs(tbl) do if field.type == 'tablefield' then source._enums[#source._enums+1] = field local subType = vm.declareGlobal('type', name .. '.' .. field.field[1], uri) subType:addSet(uri, field) elseif field.type == 'tableindex' then source._enums[#source._enums+1] = field if field.index.type == 'string' then local subType = vm.declareGlobal('type', name .. '.' .. field.index[1], uri) subType:addSet(uri, field) end end end end end) : case 'doc.type.name' : call(function (source) local uri = guide.getUri(source) local name = source[1] if name == '_' then return end if name == 'self' then return end local type = vm.declareGlobal('type', name, uri) type:addGet(uri, source) source._globalNode = type end) : case 'doc.extends.name' : call(function (source) local uri = guide.getUri(source) local name = source[1] local class = vm.declareGlobal('type', name, uri) class:addGet(uri, source) source._globalNode = class end) ---@alias vm.global.cate '"variable"' | '"type"' ---@param cate vm.global.cate ---@param name string ---@param uri? uri ---@return vm.global function vm.declareGlobal(cate, name, uri) local key = cate .. '|' .. name if uri then globalSubs[uri][key] = true end if not allGlobals[key] then allGlobals[key] = createGlobal(name, cate) end return allGlobals[key] end ---@param cate vm.global.cate ---@param name string ---@param field? string ---@return vm.global? function vm.getGlobal(cate, name, field) local key = cate .. '|' .. name if field then key = key .. vm.ID_SPLITE .. field end return allGlobals[key] end ---@param cate vm.global.cate ---@param name string ---@return vm.global[] function vm.getGlobalFields(cate, name) local globals = {} local key = cate .. '|' .. name local clock = os.clock() for gid, global in pairs(allGlobals) do if gid ~= key and util.stringStartWith(gid, key) and gid:sub(#key + 1, #key + 1) == vm.ID_SPLITE and not gid:find(vm.ID_SPLITE, #key + 2) then globals[#globals+1] = global end end local cost = os.clock() - clock if cost > 0.1 then log.warn('global-manager getFields costs', cost) end return globals end ---@param cate vm.global.cate ---@return vm.global[] function vm.getGlobals(cate) local globals = {} local clock = os.clock() for gid, global in pairs(allGlobals) do if util.stringStartWith(gid, cate) and not gid:find(vm.ID_SPLITE) then globals[#globals+1] = global end end local cost = os.clock() - clock if cost > 0.1 then log.warn('global-manager getGlobals costs', cost) end return globals end ---@return table function vm.getAllGlobals() return allGlobals end ---@param suri uri ---@param cate vm.global.cate ---@return parser.object[] function vm.getGlobalSets(suri, cate) local globals = vm.getGlobals(cate) local result = {} for _, global in ipairs(globals) do local sets = global:getSets(suri) for _, set in ipairs(sets) do result[#result+1] = set end end return result end ---@param suri uri ---@param cate vm.global.cate ---@param name string ---@return boolean function vm.hasGlobalSets(suri, cate, name) local global = vm.getGlobal(cate, name) if not global then return false end local sets = global:getSets(suri) if #sets == 0 then return false end return true end ---@param uri uri ---@param key string ---@return boolean local function checkIsGlobalRegex(uri, key) local dglobalsregex = config.get(uri, 'Lua.diagnostics.globalsRegex') if not dglobalsregex then return false end for _, pattern in ipairs(dglobalsregex) do if key:match(pattern) then return true end end return false end ---@param src parser.object local function checkIsUndefinedGlobal(src) if src.type ~= 'getglobal' then return false end local key = src[1] if not key then return false end local node = src.node if node.tag ~= '_ENV' then return false end local uri = guide.getUri(src) local rspecial = config.get(uri, 'Lua.runtime.special') if rspecial[key] then return false end if vm.hasGlobalSets(uri, 'variable', key) then return false end local dglobals = config.get(uri, 'Lua.diagnostics.globals') if util.arrayHas(dglobals, key) then return false end if checkIsGlobalRegex(uri, key) then return false end return true end ---@param src parser.object ---@return boolean function vm.isUndefinedGlobal(src) local node = vm.compileNode(src) if node.undefinedGlobal == nil then node.undefinedGlobal = checkIsUndefinedGlobal(src) end return node.undefinedGlobal end ---@param source parser.object function compileObject(source) if source._globalNode ~= nil then return end source._globalNode = false compilerGlobalSwitch(source.type, source) end ---@param source parser.object ---@return vm.global? function vm.getGlobalNode(source) return source._globalNode or nil end ---@param source parser.object ---@return parser.object[]? function vm.getEnums(source) return source._enums end ---@param source parser.object ---@return boolean function vm.compileByGlobal(source) local global = vm.getGlobalNode(source) if not global then return false end vm.setNode(source, global) if global.cate == 'variable' then if guide.isAssign(source) then if vm.bindDocs(source) then return true end if source.value and source.value.type ~= 'nil' then vm.setNode(source, vm.compileNode(source.value)) return true end else if vm.bindAs(source) then return true end local node = vm.traceNode(source) if node then vm.setNode(source, node, true) return true end end end local globalBase = vm.getGlobalBase(source) if not globalBase then return false end local globalNode = vm.compileNode(globalBase) vm.setNode(source, globalNode, true) return true end ---@param source parser.object ---@return parser.object? function vm.getGlobalBase(source) if source._globalBase then return source._globalBase end local global = vm.getGlobalNode(source) if not global then return nil end ---@cast source parser.object local root = guide.getRoot(source) if not root._globalBaseMap then root._globalBaseMap = {} end local name = global:asKeyName() if not root._globalBaseMap[name] then ---@diagnostic disable-next-line: missing-fields root._globalBaseMap[name] = { type = 'globalbase', parent = root, global = global, start = 0, finish = 0, } end source._globalBase = root._globalBaseMap[name] return source._globalBase end ---@param source parser.object local function compileAst(source) local env = guide.getENV(source) if not env then return end compileObject(env) guide.eachSpecialOf(source, 'rawset', function (src) compileObject(src.parent) end) guide.eachSpecialOf(source, 'rawget', function (src) compileObject(src.parent) end) guide.eachSourceTypes(source.docs, { 'doc.class', 'doc.alias', 'doc.type.name', 'doc.extends.name', 'doc.enum', }, function (src) compileObject(src) end) end ---@param uri uri local function dropUri(uri) local globalSub = globalSubs[uri] globalSubs[uri] = nil for key in pairs(globalSub) do local global = allGlobals[key] if global then global:dropUri(uri) if not global:isAlive() then allGlobals[key] = nil end end end end return { compileAst = compileAst, dropUri = dropUri, }