Skip to content

Commit 51b5771

Browse files
committed
Add boolean-context diagnostic
1 parent 088bc28 commit 51b5771

11 files changed

Lines changed: 177 additions & 0 deletions

File tree

locale/en-us/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'The return values of this function cannot be discarded.'
108108
DIAG_NEED_CHECK_NIL =
109109
'Need check nil.'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'This expression is always {}.'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'Boolean context expects boolean, got {}.'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'Circularly inherited classes.'
112116
DIAG_DOC_FIELD_NO_CLASS =

locale/es-419/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'No se pueden descartar los valores retornados por esta función.'
108108
DIAG_NEED_CHECK_NIL =
109109
'Un chequeo de nil es necesario.'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'Esta expresión siempre es {}.'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'El contexto booleano espera boolean, se obtuvo {}.'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'Clases con herencia circular.'
112116
DIAG_DOC_FIELD_NO_CLASS =

locale/ja-jp/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'この関数の戻り値は破棄できません。'
108108
DIAG_NEED_CHECK_NIL =
109109
'nil チェックが必要です。'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'この式は常に {} です。'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'この箇所は boolean を期待していますが、{} です。'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'循環継承されたクラスです。'
112116
DIAG_DOC_FIELD_NO_CLASS =

locale/pt-br/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'Os valores retornados desta função não podem ser descartáveis.'
108108
DIAG_NEED_CHECK_NIL =
109109
'Necessário checar o nil.'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'Esta expressão é sempre {}.'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'O contexto booleano espera boolean, recebido {}.'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'Classes com herança cíclica.'
112116
DIAG_DOC_FIELD_NO_CLASS =

locale/zh-cn/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'不能丢弃此函数的返回值。'
108108
DIAG_NEED_CHECK_NIL =
109109
'需要判空。'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'该表达式始终为 {}。'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'此处需要 boolean,但得到 {}。'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'循环继承的类。'
112116
DIAG_DOC_FIELD_NO_CLASS =

locale/zh-tw/script.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ DIAG_DISCARD_RETURNS =
107107
'不能丟棄此函式的回傳值。'
108108
DIAG_NEED_CHECK_NIL =
109109
'需要判空'
110+
DIAG_BOOLEAN_CONTEXT_ALWAYS =
111+
'此表達式始終為 {}。'
112+
DIAG_BOOLEAN_CONTEXT_NONBOOLEAN =
113+
'此處需要 boolean,但得到 {}。'
110114
DIAG_CIRCLE_DOC_CLASS =
111115
'循環繼承的類別。'
112116
DIAG_DOC_FIELD_NO_CLASS =
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
local files = require 'files'
2+
local guide = require 'parser.guide'
3+
local vm = require 'vm'
4+
local lang = require 'language'
5+
local await = require 'await'
6+
7+
---@param node vm.node
8+
---@return string|nil
9+
local function getTruthiness(node)
10+
if node:alwaysTruthy() then
11+
return 'true'
12+
end
13+
if node:alwaysFalsy() then
14+
return 'false'
15+
end
16+
return nil
17+
end
18+
19+
---@param infer vm.infer
20+
---@param uri uri
21+
---@return boolean|nil
22+
local function isBooleanOnly(infer, uri)
23+
if infer:hasAny(uri) or infer:hasUnknown(uri) then
24+
return nil
25+
end
26+
local hasBoolean = infer:hasType(uri, 'boolean')
27+
if not hasBoolean then
28+
return false
29+
end
30+
for view in infer:eachView(uri) do
31+
if view ~= 'boolean' then
32+
return false
33+
end
34+
end
35+
return true
36+
end
37+
38+
---@param source parser.object?
39+
---@param uri uri
40+
---@param callback fun(result: diag.result)
41+
local function checkExpression(source, uri, callback)
42+
if not source then
43+
return
44+
end
45+
local node = vm.compileNode(source)
46+
local truthiness = getTruthiness(node)
47+
if truthiness then
48+
callback {
49+
start = source.start,
50+
finish = source.finish,
51+
message = lang.script('DIAG_BOOLEAN_CONTEXT_ALWAYS', truthiness),
52+
}
53+
return
54+
end
55+
local infer = vm.getInfer(source)
56+
local onlyBoolean = isBooleanOnly(infer, uri)
57+
if onlyBoolean == true or onlyBoolean == nil then
58+
return
59+
end
60+
callback {
61+
start = source.start,
62+
finish = source.finish,
63+
message = lang.script('DIAG_BOOLEAN_CONTEXT_NONBOOLEAN', infer:view(uri)),
64+
}
65+
end
66+
67+
---@async
68+
return function (uri, callback)
69+
local state = files.getState(uri)
70+
if not state then
71+
return
72+
end
73+
74+
---@async
75+
guide.eachSourceTypes(state.ast, {'ifblock', 'elseifblock', 'while', 'repeat'}, function (source)
76+
await.delay()
77+
if source.filter
78+
and source.filter.type == 'binary'
79+
and source.filter.op
80+
and (source.filter.op.type == 'and' or source.filter.op.type == 'or') then
81+
return
82+
end
83+
checkExpression(source.filter, uri, callback)
84+
end)
85+
86+
---@async
87+
guide.eachSourceType(state.ast, 'binary', function (source)
88+
await.delay()
89+
local op = source.op and source.op.type
90+
if op ~= 'and' and op ~= 'or' then
91+
return
92+
end
93+
checkExpression(source[1], uri, callback)
94+
end)
95+
end

script/proto/diagnostic.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ m.register {
7171

7272
m.register {
7373
'need-check-nil',
74+
'boolean-context',
7475
'undefined-field',
7576
'cast-local-type',
7677
'assign-type-mismatch',

script/vm/node.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,29 @@ function mt:alwaysTruthy()
145145
return true
146146
end
147147

148+
---Almost an inverse of alwaysTruthy, but strict about "any" and "unknown" types.
149+
---@return boolean
150+
function mt:alwaysFalsy()
151+
if self.optional then
152+
return false
153+
end
154+
if #self == 0 then
155+
return false
156+
end
157+
for _, c in ipairs(self) do
158+
if c.type == 'nil'
159+
or (c.type == 'global' and c.cate == 'type' and c.name == 'nil')
160+
or (c.type == 'global' and c.cate == 'type' and c.name == 'false')
161+
or (c.type == 'boolean' and c[1] == false)
162+
or (c.type == 'doc.type.boolean' and c[1] == false) then
163+
-- ok
164+
else
165+
return false
166+
end
167+
end
168+
return true
169+
end
170+
148171
---@return boolean
149172
function mt:hasKnownType()
150173
for _, c in ipairs(self) do
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
TEST [[
2+
---@type number
3+
local a
4+
if <!a!> then end
5+
6+
---@type number
7+
local b
8+
if <!not b!> then end
9+
10+
---@type number
11+
local c
12+
local x = <!c!> or "3"
13+
14+
---@type false
15+
local d
16+
local y = <!d!> and "4"
17+
18+
---@type boolean
19+
local e
20+
if e then end
21+
22+
---@type boolean|nil
23+
local g
24+
if <!g!> then end
25+
26+
---@type number
27+
local h
28+
if <!h!> and true then end
29+
30+
---@type any
31+
local f
32+
local z = f or "3"
33+
]]

0 commit comments

Comments
 (0)