From f777e326716005c65aab50dfb2b974cf983021e2 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Sep 2021 12:10:01 +0800 Subject: add typecheck --- script/core/diagnostics/type-check.lua | 132 +++++++++++++++++++++++++++++++++ script/proto/define.lua | 1 + test/diagnostics/init.lua | 67 ++++++++++++++++- 3 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 script/core/diagnostics/type-check.lua diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua new file mode 100644 index 00000000..99b40303 --- /dev/null +++ b/script/core/diagnostics/type-check.lua @@ -0,0 +1,132 @@ +local files = require 'files' +local guide = require 'parser.guide' +local vm = require 'vm' + +local function hasTypeDoc(obj) + if obj.type == 'getlocal' + and obj.node + and obj.node.type == 'local' + and obj.node.bindDocs + and obj.node.bindDocs[1] + and obj.node.bindDocs[1].type == 'doc.type' then + return true + end + return false +end +local function inTypes(param, args) + for _, v in ipairs(args.type) do + if param[1] == v[1] then + return true + end + end + return false +end +return function (uri, callback) + local ast = files.getState(uri) + if not ast then + return + end + guide.eachSourceType(ast.ast, 'call', function (source) + if not source.args then + return + end + local callArgs = source.args + local callArgsType = {} + for _, arg in ipairs(callArgs) do + ---检查字面值类型的参数调用 + if guide.isLiteral(arg) then + callArgsType[#callArgsType+1] = { + type = { + [1] = { + [1] = arg.type, + ['type'] = arg.type, + } + }, + start = arg.start, + finish = arg.finish, + } + ---检查传入参数有完整信息的情况 + elseif hasTypeDoc(arg) then + callArgsType[#callArgsType+1] = { + type = arg.node.bindDocs[1].types, + start = arg.start, + finish = arg.finish, + } + else + return + end + end + local func = source.node + local defs = vm.getDefs(func) + local funcArgsType = {} + ---只检查有emmy注释定义的函数 + ---获取函数定义的参数信息时,遇到一个定义就停止获取 + for _, def in ipairs(defs) do + if def.value then + def = def.value + end + if def.type == 'function' then + if def.args then + for _, arg in ipairs(def.args) do + if arg.docParam and arg.docParam.extends then + ---如果是很复杂的type,比如泛型什么的,先不检查 + if not arg.docParam.extends.types[1][1] + or arg.docParam.extends.types[1].typeGeneric then + return + end + funcArgsType[#funcArgsType+1] = arg.docParam.extends.types + else + funcArgsType = {} + end + end + end + break + end + end + if #funcArgsType == 0 then + return + end + local message = '' + ---先遍历实参 + for i, arg in ipairs(callArgsType) do + if not funcArgsType[i] then + ---由另一个检查来处理 + -- message = 'too many call args' + -- callback{ + -- start = arg.start, + -- finish = arg.finish, + -- message = message + -- } + return + end + local flag = '' + ---遍历形参 + for _, tp in ipairs(funcArgsType[i]) do + ---如果形参的类型在实参里面 + if inTypes(tp, arg) + or tp[1] == 'any' + or arg.type == 'any' then + flag = '' + break + else + flag = flag ..' ' .. tp[1] + end + end + if flag ~= '' then + local argm = '[ ' + for _, v in ipairs(arg.type) do + argm = argm .. v[1]..' ' + end + argm = argm .. ']' + message = 'callArg: '..argm..' has no type belong to ['..flag..' ]' + callback{ + start = arg.start, + finish = arg.finish, + message = message + } + return + end + end + end) + +end diff --git a/script/proto/define.lua b/script/proto/define.lua index 2e61bc3e..2e4116fd 100644 --- a/script/proto/define.lua +++ b/script/proto/define.lua @@ -49,6 +49,7 @@ m.DiagnosticSeverity = { --- 诊断类型与默认等级 ---@type table m.DiagnosticDefaultSeverity = { + ['type-check'] = "Hint", ['unused-local'] = 'Hint', ['unused-function'] = 'Hint', ['undefined-global'] = 'Warning', diff --git a/test/diagnostics/init.lua b/test/diagnostics/init.lua index 54ac73ef..79b267c3 100644 --- a/test/diagnostics/init.lua +++ b/test/diagnostics/init.lua @@ -491,7 +491,7 @@ f(1, 2, 3) ]] TEST [[ -(1) +() ]] TEST [[ @@ -1164,3 +1164,68 @@ return { [] = 4, } ]] + +TEST [[ +---@param table table +---@param metatable table +---@return table +function Setmetatable(table, metatable) end + +Setmetatable(, {}) +]] + +TEST [[ +---@param table table +---@param metatable table +---@return table +function Setmetatable(table, metatable) end + +Setmetatable(, {}) + +]] + +TEST [[ +---@param table table +---@param metatable table +---@return table +function Setmetatable(table, metatable) end + +---@type table +local name +---@type function +local mt +---err +Setmetatable(name, ) +]] + +TEST [[ +---@param p1 string +---@param p2 number +---@return table +local function func1(p1, p2) end + +---@type string +local s +---@type table +local t +---err +func1(s, ) +]] + +TEST [[ +---@class bird +---@field wing string + +---@class eagle +---@field family bird + +---@class chicken +---@field family bird + +---@param bd eagle +local function fly(bd) end + +---@type chicken +local h +fly() +]] -- cgit v1.2.3 From c1d8340fd6ce6703b9abd187367f8093e01d5f3a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 23 Sep 2021 16:07:34 +0800 Subject: improve message --- script/core/diagnostics/type-check.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua index 99b40303..010764ba 100644 --- a/script/core/diagnostics/type-check.lua +++ b/script/core/diagnostics/type-check.lua @@ -118,7 +118,7 @@ return function (uri, callback) argm = argm .. v[1]..' ' end argm = argm .. ']' - message = 'callArg: '..argm..' has no type belong to ['..flag..' ]' + message = 'Argument of type in '..argm..' is not assignable to parameter of type in ['..flag..' ]' callback{ start = arg.start, finish = arg.finish, -- cgit v1.2.3 From d330c9020e299e9bddbadb12e3712679029febe6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 15:43:38 +0800 Subject: improve --- script/core/diagnostics/type-check.lua | 197 +++++++++++++++++++++------------ script/proto/define.lua | 2 +- 2 files changed, 126 insertions(+), 73 deletions(-) diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua index 010764ba..f021b1e5 100644 --- a/script/core/diagnostics/type-check.lua +++ b/script/core/diagnostics/type-check.lua @@ -1,20 +1,25 @@ local files = require 'files' local guide = require 'parser.guide' local vm = require 'vm' +local infer = require 'core.infer' +local await = require 'await' +-- local function hasTypeDoc(obj) +-- if obj.type == 'getlocal' +-- and obj.node +-- and obj.node.type == 'local' +-- and obj.node.bindDocs +-- and obj.node.bindDocs[1] +-- and (obj.node.bindDocs[1].type == 'doc.type' +-- or obj.node.bindDocs[1].type == 'doc.param') then +-- return true +-- end +-- return false +-- end +local function hasInferType(obj) -local function hasTypeDoc(obj) - if obj.type == 'getlocal' - and obj.node - and obj.node.type == 'local' - and obj.node.bindDocs - and obj.node.bindDocs[1] - and obj.node.bindDocs[1].type == 'doc.type' then - return true - end - return false end local function inTypes(param, args) - for _, v in ipairs(args.type) do + for _, v in ipairs(args) do if param[1] == v[1] then return true end @@ -26,6 +31,7 @@ return function (uri, callback) if not ast then return end + await.delay() guide.eachSourceType(ast.ast, 'call', function (source) if not source.args then return @@ -36,97 +42,144 @@ return function (uri, callback) ---检查字面值类型的参数调用 if guide.isLiteral(arg) then callArgsType[#callArgsType+1] = { - type = { - [1] = { - [1] = arg.type, - ['type'] = arg.type, - } + [1] = { + [1] = arg.type, + ['type'] = arg.type, }, start = arg.start, finish = arg.finish, } - ---检查传入参数有完整信息的情况 - elseif hasTypeDoc(arg) then - callArgsType[#callArgsType+1] = { - type = arg.node.bindDocs[1].types, - start = arg.start, - finish = arg.finish, - } + -- ---检查传入参数有完整信息的情况 + -- elseif hasInferType(arg) then + -- callArgsType[#callArgsType+1] = { + -- type = arg.node.bindDocs[1].types, + -- start = arg.start, + -- finish = arg.finish, + -- } else - return + local infers = infer.searchInfers(arg) + if infers['_G'] or infer['_ENV'] then + infers['_G'] = nil + infers['_ENV'] = nil + infers['table'] = true + end + local types = {} + for k in pairs(infers) do + if k then + types[#types+1] = { + [1] = k, + type = k + } + end + end + if #types < 1 then + return + end + types.start = arg.start + types.finish = arg.finish + callArgsType[#callArgsType+1] = types end end local func = source.node local defs = vm.getDefs(func) - local funcArgsType = {} + local funcArgsType + local mark = {} + local paramType = {} ---只检查有emmy注释定义的函数 - ---获取函数定义的参数信息时,遇到一个定义就停止获取 for _, def in ipairs(defs) do + funcArgsType = {} if def.value then def = def.value end - if def.type == 'function' then + if mark[def] then + goto CONTINUE + end + mark[def] = true + if def.type == 'function' + or def.type == 'doc.type.function' then if def.args then for _, arg in ipairs(def.args) do + local types if arg.docParam and arg.docParam.extends then - ---如果是很复杂的type,比如泛型什么的,先不检查 - if not arg.docParam.extends.types[1][1] - or arg.docParam.extends.types[1].typeGeneric then - return - end - funcArgsType[#funcArgsType+1] = arg.docParam.extends.types + types = arg.docParam.extends.types + ---变长参数不检查 + elseif arg.name and arg.name[1] == '...' then + return + elseif arg.type == 'doc.type.arg' then + types = arg.extends.types else - funcArgsType = {} + goto CONTINUE + end + + ---如果是很复杂的type,比如泛型什么的,先不检查 + if not types[1] + or not types[1][1] + or types[1].typeGeneric then + goto CONTINUE end + funcArgsType[#funcArgsType+1] = types end end - break + else + goto CONTINUE end - end - if #funcArgsType == 0 then - return - end - local message = '' - ---先遍历实参 - for i, arg in ipairs(callArgsType) do - if not funcArgsType[i] then - ---由另一个检查来处理 - -- message = 'too many call args' - -- callback{ - -- start = arg.start, - -- finish = arg.finish, - -- message = message - -- } - return + if #funcArgsType == 0 then + goto CONTINUE end - local flag = '' - ---遍历形参 - for _, tp in ipairs(funcArgsType[i]) do - ---如果形参的类型在实参里面 - if inTypes(tp, arg) - or tp[1] == 'any' - or arg.type == 'any' then - flag = '' - break - else - flag = flag ..' ' .. tp[1] + paramType[#paramType+1] = funcArgsType + ::CONTINUE:: + end + ---先遍历实参 + for i, arg in ipairs(callArgsType) do + local flag = '' + local messages = {} + ---遍历形参 + for _, par in ipairs(paramType) do + if not par[i] then + ---实参过多,由另一个检查来处理,此处跳过 + goto CONTINUE + end + for _, param in ipairs(par[i]) do + ---如果形参的类型在实参里面 + if inTypes(param, arg) + or param[1] == 'any' + or arg.type == 'any' then + flag = '' + goto HIT + else + flag = flag ..' ' .. param[1] + end + end + if flag ~= '' then + local argm = '[ ' + for _, v in ipairs(arg) do + argm = argm .. v[1]..' ' + end + argm = argm .. ']' + local message = 'Argument of type in '..argm..' is not assignable to parameter of type in ['..flag..' ]' + messages[#messages+1] = message + end + ::CONTINUE:: end - end - if flag ~= '' then - local argm = '[ ' - for _, v in ipairs(arg.type) do - argm = argm .. v[1]..' ' + ---都不匹配 + local norepeat = {} + for _, m in ipairs(messages) do + if not norepeat[m] then + norepeat[m] = true + norepeat[#norepeat+1] = m + end + end + if #norepeat == 0 then + return end - argm = argm .. ']' - message = 'Argument of type in '..argm..' is not assignable to parameter of type in ['..flag..' ]' callback{ start = arg.start, finish = arg.finish, - message = message + message = table.concat(norepeat, '\n') } - return + ::HIT:: end - end + ---所有参数都匹配了 end) end diff --git a/script/proto/define.lua b/script/proto/define.lua index fe7d171b..58eafe2a 100644 --- a/script/proto/define.lua +++ b/script/proto/define.lua @@ -17,7 +17,7 @@ m.DiagnosticSeverity = { --- 诊断类型与默认等级 ---@type table m.DiagnosticDefaultSeverity = { - ['type-check'] = "Hint", + ['type-check'] = "Warning", ['unused-local'] = 'Hint', ['unused-function'] = 'Hint', ['undefined-global'] = 'Warning', -- cgit v1.2.3 From 0c8da1979087774eb1b352188762e7b3345c1089 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 15:45:02 +0800 Subject: remove comments --- script/core/diagnostics/type-check.lua | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua index f021b1e5..357beee0 100644 --- a/script/core/diagnostics/type-check.lua +++ b/script/core/diagnostics/type-check.lua @@ -3,21 +3,7 @@ local guide = require 'parser.guide' local vm = require 'vm' local infer = require 'core.infer' local await = require 'await' --- local function hasTypeDoc(obj) --- if obj.type == 'getlocal' --- and obj.node --- and obj.node.type == 'local' --- and obj.node.bindDocs --- and obj.node.bindDocs[1] --- and (obj.node.bindDocs[1].type == 'doc.type' --- or obj.node.bindDocs[1].type == 'doc.param') then --- return true --- end --- return false --- end -local function hasInferType(obj) -end local function inTypes(param, args) for _, v in ipairs(args) do if param[1] == v[1] then @@ -49,13 +35,6 @@ return function (uri, callback) start = arg.start, finish = arg.finish, } - -- ---检查传入参数有完整信息的情况 - -- elseif hasInferType(arg) then - -- callArgsType[#callArgsType+1] = { - -- type = arg.node.bindDocs[1].types, - -- start = arg.start, - -- finish = arg.finish, - -- } else local infers = infer.searchInfers(arg) if infers['_G'] or infer['_ENV'] then -- cgit v1.2.3 From bab5ab685d64258d0f8fe10e505c5d7837307f7b Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 17:05:43 +0800 Subject: improve --- script/core/diagnostics/type-check.lua | 260 +++++++++++++++++---------------- 1 file changed, 135 insertions(+), 125 deletions(-) diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua index 357beee0..7b186fa0 100644 --- a/script/core/diagnostics/type-check.lua +++ b/script/core/diagnostics/type-check.lua @@ -3,6 +3,7 @@ local guide = require 'parser.guide' local vm = require 'vm' local infer = require 'core.infer' local await = require 'await' +local hasVarargs local function inTypes(param, args) for _, v in ipairs(args) do @@ -12,153 +13,162 @@ local function inTypes(param, args) end return false end + +local function excludeSituations(types) + if not types[1] + or not types[1][1] + or types[1].typeGeneric then + return true + end + return false +end + +local function getInfoFromDefs(defs) + local paramsTypes = {} + local funcArgsType + local mark = {} + for _, def in ipairs(defs) do + funcArgsType = {} + if def.value then + def = def.value + end + if mark[def] then + goto CONTINUE + end + mark[def] = true + + if def.type == 'function' + or def.type == 'doc.type.function' then + if def.args then + for _, arg in ipairs(def.args) do + local types + if arg.docParam and arg.docParam.extends then + types = arg.docParam.extends.types + ---变长参数 + elseif arg.name and arg.name[1] == '...' then + types = { + [1] = { + [1] = '...', + type = 'varargs' + } + } + elseif arg.type == 'doc.type.arg' then + types = arg.extends.types + else + goto CONTINUE + end + ---如果是很复杂的type,比如泛型什么的,先不检查 + if excludeSituations(types) then + goto CONTINUE + end + funcArgsType[#funcArgsType+1] = types + end + end + if #funcArgsType > 0 then + paramsTypes[#paramsTypes+1] = funcArgsType + end + end + ::CONTINUE:: + end + return paramsTypes +end + +local function matchParams(paramsTypes, i, arg) + local flag = '' + local messages = {} + for _, paramTypes in ipairs(paramsTypes) do + if not paramTypes[i] then + goto CONTINUE + end + for _, param in ipairs(paramTypes[i]) do + ---如果形参的类型在实参里面 + if inTypes(param, arg) + or param[1] == 'any' + or arg.type == 'any' then + flag = '' + return true + elseif param[1] == '...' then + hasVarargs = true + return true + else + flag = flag ..' ' .. param[1] + end + end + if flag ~= '' then + local argm = '[ ' + for _, v in ipairs(arg) do + argm = argm .. v[1]..' ' + end + argm = argm .. ']' + local message = 'Argument of type in '..argm..' is not assignable to parameter of type in ['..flag..' ]' + if not messages[message] then + messages[message] = true + messages[#messages+1] = message + end + end + ::CONTINUE:: + end + return false, messages +end return function (uri, callback) local ast = files.getState(uri) if not ast then return end - await.delay() guide.eachSourceType(ast.ast, 'call', function (source) if not source.args then return end + await.delay() local callArgs = source.args local callArgsType = {} for _, arg in ipairs(callArgs) do - ---检查字面值类型的参数调用 - if guide.isLiteral(arg) then - callArgsType[#callArgsType+1] = { - [1] = { - [1] = arg.type, - ['type'] = arg.type, - }, - start = arg.start, - finish = arg.finish, - } - else - local infers = infer.searchInfers(arg) - if infers['_G'] or infer['_ENV'] then - infers['_G'] = nil - infers['_ENV'] = nil - infers['table'] = true - end - local types = {} - for k in pairs(infers) do - if k then - types[#types+1] = { - [1] = k, - type = k - } - end - end - if #types < 1 then - return + local infers = infer.searchInfers(arg) + if infers['_G'] or infer['_ENV'] then + infers['_G'] = nil + infers['_ENV'] = nil + infers['table'] = true + end + local types = {} + for k in pairs(infers) do + if k then + types[#types+1] = { + [1] = k, + type = k + } end - types.start = arg.start - types.finish = arg.finish - callArgsType[#callArgsType+1] = types end + if #types < 1 then + return + end + types.start = arg.start + types.finish = arg.finish + callArgsType[#callArgsType+1] = types end local func = source.node local defs = vm.getDefs(func) - local funcArgsType - local mark = {} - local paramType = {} ---只检查有emmy注释定义的函数 - for _, def in ipairs(defs) do - funcArgsType = {} - if def.value then - def = def.value - end - if mark[def] then - goto CONTINUE + local paramsTypes = getInfoFromDefs(defs) + ---遍历实参 + for i, arg in ipairs(callArgsType) do + ---遍历形参 + hasVarargs = false + local match, messages = matchParams(paramsTypes, i, arg) + if hasVarargs then + return end - mark[def] = true - if def.type == 'function' - or def.type == 'doc.type.function' then - if def.args then - for _, arg in ipairs(def.args) do - local types - if arg.docParam and arg.docParam.extends then - types = arg.docParam.extends.types - ---变长参数不检查 - elseif arg.name and arg.name[1] == '...' then - return - elseif arg.type == 'doc.type.arg' then - types = arg.extends.types - else - goto CONTINUE - end - - ---如果是很复杂的type,比如泛型什么的,先不检查 - if not types[1] - or not types[1][1] - or types[1].typeGeneric then - goto CONTINUE - end - funcArgsType[#funcArgsType+1] = types - end + ---都不匹配 + if not match then + if #messages > 0 then + callback{ + start = arg.start, + finish = arg.finish, + message = table.concat(messages, '\n') + } end - else - goto CONTINUE - end - if #funcArgsType == 0 then - goto CONTINUE end - paramType[#paramType+1] = funcArgsType - ::CONTINUE:: end - ---先遍历实参 - for i, arg in ipairs(callArgsType) do - local flag = '' - local messages = {} - ---遍历形参 - for _, par in ipairs(paramType) do - if not par[i] then - ---实参过多,由另一个检查来处理,此处跳过 - goto CONTINUE - end - for _, param in ipairs(par[i]) do - ---如果形参的类型在实参里面 - if inTypes(param, arg) - or param[1] == 'any' - or arg.type == 'any' then - flag = '' - goto HIT - else - flag = flag ..' ' .. param[1] - end - end - if flag ~= '' then - local argm = '[ ' - for _, v in ipairs(arg) do - argm = argm .. v[1]..' ' - end - argm = argm .. ']' - local message = 'Argument of type in '..argm..' is not assignable to parameter of type in ['..flag..' ]' - messages[#messages+1] = message - end - ::CONTINUE:: - end - ---都不匹配 - local norepeat = {} - for _, m in ipairs(messages) do - if not norepeat[m] then - norepeat[m] = true - norepeat[#norepeat+1] = m - end - end - if #norepeat == 0 then - return - end - callback{ - start = arg.start, - finish = arg.finish, - message = table.concat(norepeat, '\n') - } - ::HIT:: - end - ---所有参数都匹配了 + ---所有参数都匹配了 end) end -- cgit v1.2.3 From 626032b400af1cf79823a9a004ad2d0937599153 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Sep 2021 17:44:16 +0800 Subject: add test --- script/core/diagnostics/type-check.lua | 3 +++ test/diagnostics/init.lua | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/script/core/diagnostics/type-check.lua b/script/core/diagnostics/type-check.lua index 7b186fa0..ff3a9124 100644 --- a/script/core/diagnostics/type-check.lua +++ b/script/core/diagnostics/type-check.lua @@ -9,6 +9,9 @@ local function inTypes(param, args) for _, v in ipairs(args) do if param[1] == v[1] then return true + elseif param[1] == 'number' + and v[1] == 'integer' then + return true end end return false diff --git a/test/diagnostics/init.lua b/test/diagnostics/init.lua index e3bf9516..1b26c00b 100644 --- a/test/diagnostics/init.lua +++ b/test/diagnostics/init.lua @@ -1198,3 +1198,14 @@ local function fly(bd) end local h fly() ]] + +TEST [[ +---@overload fun(x: number, y: number) +---@param x boolean +---@param y boolean +local function f(x, y) end + +f(true, true) -- OK +f(0, 0) -- OK + +]] \ No newline at end of file -- cgit v1.2.3