Skip to content

Diagnostics for unnecessary assert #3128

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 3 commits into from
Apr 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased
<!-- Add all new changes here. They will be moved under a version at release -->
`2025-3-13`
* `NEW` `unnecessary-assert` diagnostic warns when asserting values that are always truthy
* `NEW` locale `es-419`, thanks [Felipe Lema](https://codeberg.org/FelipeLema)

## 3.13.9
Expand Down
2 changes: 2 additions & 0 deletions locale/en-us/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DIAG_OVER_MAX_ARGS =
'This function expects a maximum of {:d} argument(s) but instead it is receiving {:d}.'
DIAG_MISS_ARGS =
'This function requires {:d} argument(s) but instead it is receiving {:d}.'
DIAG_UNNECESSARY_ASSERT =
'Unnecessary assert: this expression is always truthy.'
DIAG_OVER_MAX_VALUES =
'Only has {} variables, but you set {} values.'
DIAG_AMBIGUITY_1 =
Expand Down
2 changes: 2 additions & 0 deletions locale/en-us/setting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ config.diagnostics['missing-return-value'] =
'Enable diagnostics for return statements without values although the containing function declares returns.'
config.diagnostics['need-check-nil'] =
'Enable diagnostics for variable usages if `nil` or an optional (potentially `nil`) value was assigned to the variable before.'
config.diagnostics['unnecessary-assert'] =
'Enable diagnostics for redundant assertions on truthy values.'
config.diagnostics['no-unknown'] =
'Enable diagnostics for cases in which the type cannot be inferred.'
config.diagnostics['not-yieldable'] =
Expand Down
2 changes: 2 additions & 0 deletions locale/ja-jp/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DIAG_OVER_MAX_ARGS =
'この関数は最大で {:d} 個の引数を受け取りますが、{:d} 個の引数が渡されています。'
DIAG_MISS_ARGS =
'この関数は少なくとも {:d} 個の引数を必要としますが、{:d} 個しか渡されていません。'
DIAG_UNNECESSARY_ASSERT =
'不要なアサーション: この式は常に真です。'
DIAG_OVER_MAX_VALUES =
'変数は {} 個しかありませんが、{} 個の値が設定されています。'
DIAG_AMBIGUITY_1 =
Expand Down
2 changes: 2 additions & 0 deletions locale/ja-jp/setting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ config.diagnostics['missing-return-value'] = -- TODO: need translate!
'Enable diagnostics for return statements without values although the containing function declares returns.'
config.diagnostics['need-check-nil'] = -- TODO: need translate!
'Enable diagnostics for variable usages if `nil` or an optional (potentially `nil`) value was assigned to the variable before.'
config.diagnostics['unnecessary-assert'] = -- TODO: need translate!
'Enable diagnostics for redundant assertions on truthy values.'
config.diagnostics['no-unknown'] = -- TODO: need translate!
'Enable diagnostics for cases in which the type cannot be inferred.'
config.diagnostics['not-yieldable'] = -- TODO: need translate!
Expand Down
2 changes: 2 additions & 0 deletions locale/pt-br/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DIAG_OVER_MAX_ARGS =
'A função aceita apenas os parâmetros {:d}, mas você passou {:d}.'
DIAG_MISS_ARGS =
'A função recebe pelo menos {:d} argumentos, mas há {:d}.'
DIAG_UNNECESSARY_ASSERT =
'Asserção desnecessária: esta expressão é sempre verdadeira.'
DIAG_OVER_MAX_VALUES =
'Apenas há {} variáveis, mas você declarou {} valores.'
DIAG_AMBIGUITY_1 =
Expand Down
2 changes: 2 additions & 0 deletions locale/pt-br/setting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ config.diagnostics['missing-return-value'] = -- TODO: need translate!
'Enable diagnostics for return statements without values although the containing function declares returns.'
config.diagnostics['need-check-nil'] = -- TODO: need translate!
'Enable diagnostics for variable usages if `nil` or an optional (potentially `nil`) value was assigned to the variable before.'
config.diagnostics['unnecessary-assert'] = -- TODO: need translate!
'Enable diagnostics for redundant assertions on truthy values.'
config.diagnostics['no-unknown'] = -- TODO: need translate!
'Enable diagnostics for cases in which the type cannot be inferred.'
config.diagnostics['not-yieldable'] = -- TODO: need translate!
Expand Down
2 changes: 2 additions & 0 deletions locale/zh-cn/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DIAG_OVER_MAX_ARGS =
'函数最多接收 {:d} 个参数,但获得了 {:d} 个。'
DIAG_MISS_ARGS =
'函数最少接收 {:d} 个参数,但获得了 {:d} 个。'
DIAG_UNNECESSARY_ASSERT =
'不必要的断言:此表达式始终为真值。'
DIAG_OVER_MAX_VALUES =
'只有 {} 个变量,但你设置了 {} 个值。'
DIAG_AMBIGUITY_1 =
Expand Down
2 changes: 2 additions & 0 deletions locale/zh-cn/setting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ config.diagnostics['missing-return-value'] =
'函数无值返回但函数使用`@return`标记了返回值'
config.diagnostics['need-check-nil'] =
'变量之前被赋值为`nil`或可选值(可能为 `nil`)'
config.diagnostics['unnecessary-assert'] =
'启用对冗余断言(针对始终为真值的表达式)的诊断'
config.diagnostics['no-unknown'] =
'变量的未知类型无法推断'
config.diagnostics['not-yieldable'] =
Expand Down
2 changes: 2 additions & 0 deletions locale/zh-tw/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DIAG_OVER_MAX_ARGS =
'函式最多接收 {:d} 個引數,但獲得了 {:d} 個。'
DIAG_MISS_ARGS =
'函式最少接收 {:d} 個引數,但獲得了 {:d} 個。'
DIAG_UNNECESSARY_ASSERT =
'不必要的斷言:此表達式始終為真值。'
DIAG_OVER_MAX_VALUES =
'只有 {} 個變數,但你設定了 {} 個值。'
DIAG_AMBIGUITY_1 =
Expand Down
2 changes: 2 additions & 0 deletions locale/zh-tw/setting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ config.diagnostics['missing-return-value'] = -- TODO: need translate!
'Enable diagnostics for return statements without values although the containing function declares returns.'
config.diagnostics['need-check-nil'] = -- TODO: need translate!
'Enable diagnostics for variable usages if `nil` or an optional (potentially `nil`) value was assigned to the variable before.'
config.diagnostics['unnecessary-assert'] = -- TODO: need translate!
'Enable diagnostics for redundant assertions on truthy values.'
config.diagnostics['no-unknown'] = -- TODO: need translate!
'Enable diagnostics for cases in which the type cannot be inferred.'
config.diagnostics['not-yieldable'] = -- TODO: need translate!
Expand Down
29 changes: 29 additions & 0 deletions script/core/diagnostics/unnecessary-assert.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
local files = require 'files'
local guide = require 'parser.guide'
local vm = require 'vm'
local lang = require 'language'
local await = require 'await'

---@async
return function (uri, callback)
local state = files.getState(uri)
if not state then
return
end

---@async
guide.eachSourceType(state.ast, 'call', function (source)
await.delay()
local currentFunc = guide.getParentFunction(source)
if currentFunc and source.node.special == 'assert' and source.args[1] then
local argNode = vm.compileNode(source.args[1])
if argNode:alwaysTruthy() then
callback {
start = source.node.start,
finish = source.node.finish,
message = lang.script('DIAG_UNNECESSARY_ASSERT'),
}
end
end
end)
end
1 change: 1 addition & 0 deletions script/proto/diagnostic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ m.register {
'cast-type-mismatch',
'return-type-mismatch',
'inject-field',
'unnecessary-assert',
} {
group = 'type-check',
severity = 'Warning',
Expand Down
26 changes: 26 additions & 0 deletions script/vm/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,32 @@ function mt:hasFalsy()
return false
end

---Almost an inverse of hasFalsy, but stricter about "any" and "unknown" types.
---@return boolean
function mt:alwaysTruthy()
if self.optional then
return false
end
if #self == 0 then
return false
end
for _, c in ipairs(self) do
if c.type == 'nil'
or (c.type == 'global' and c.cate == 'type' and c.name == 'nil')
or (c.type == 'global' and c.cate == 'type' and c.name == 'false')
or (c.type == 'global' and c.cate == 'type' and c.name == 'any')
or (c.type == 'global' and c.cate == 'type' and c.name == 'boolean')
or (c.type == 'global' and c.cate == 'type' and c.name == 'doc.type.boolean')
or (c.type == 'global' and c.cate == 'type' and c.name == 'unknown')
or not self:hasKnownType()
or (c.type == 'boolean' and c[1] == false)
or (c.type == 'doc.type.boolean' and c[1] == false) then
return false
end
end
return true
end

---@return boolean
function mt:hasKnownType()
for _, c in ipairs(self) do
Expand Down
1 change: 1 addition & 0 deletions test/diagnostics/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ check 'missing-parameter'
check 'missing-return-value'
check 'missing-return'
check 'need-check-nil'
check 'unnecessary-assert'
check 'newfield-call'
check 'newline-call'
check 'not-yieldable'
Expand Down
56 changes: 56 additions & 0 deletions test/diagnostics/unnecessary-assert.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
TEST [[
local a
assert(a)

---@type boolean
local b
assert(b)

---@type any
local c
assert(c)

---@type unknown
local d
assert(d)

---@type boolean
local e
assert(e)

---@type number?
local f
assert(f)

assert(false)

assert(nil and 5)

---@return string?, string?
local function f() end

assert(f())
]]

TEST [[
<!assert!>(true)
]]

TEST [[
---@return integer
local function hi()
return 1
end
<!assert!>(hi(1))
]]

TEST [[
<!assert!>({}, 'hi')
]]

TEST [[
---@return string, string?
local function f() end

<!assert!>(f())
]]
Loading