unused function lint for recursive functions - #2553
Conversation
nnullcolumn
commented
Jul 14, 2026
- updates code and tests so that functions which are used inside of their own body but unused outside of it get linted
- closes unused function lint fails for recursive function #2550
- 100% human authored
hgoldstein
left a comment
There was a problem hiding this comment.
This needs to be flagged, but otherwise I like the direction.
| l.function = true; | ||
|
|
||
| return true; | ||
| l.scopeDepth++; |
There was a problem hiding this comment.
If I write:
local function foobar() -- Let's call this foobar_0
local function foobar() -- And this foobar_1
return foobar()
end
end
return foobar()... foobar_0 and foobar_1 are different AstLocal*s. Luau's parser handles this sorta thing for you. I don't think you need to track scope depth here at all.
There was a problem hiding this comment.
scope depth was the chosen mechanism for detecting whether the reference was inside the body of the function or not. i think i could change this to use the Location of the function's body, instead? at the time it seemed the most intuitive to me (i think it wasn't meant to get around anything in the local case, i more or less use the depth like a boolean here but that seems less intuitive than using the span of the body now)
There was a problem hiding this comment.
You could probably get away with a single boolean flag is my point, there shouldn't be a way to see a scope depth that isn't either 0 or 1.
There was a problem hiding this comment.
sorry i don't think i communicated very well; yeah, i could, but there's a rare case i'd actually probably like to add tests for. it might break if someone wrote speculative code at runtime that changes the definition:
local function fun()
if cond then
-- same local
function fun()
end
-- exits scope
else
-- same local
function fun()
end
-- exists scope
end
-- function considered used
return fun()
end
-- sets the scope to false even though it already leftadmittedly im sure this pattern is pretty rare in user code, but as a general thing i often prefer counters or spans over straight up booleans when traversing things because it makes the worstcase of "things are stepping over eachother in ways we didn't expect or that this code wasnt originally written for" much less catastrophic. this might also be more likely e.g. if someone updated the lint to support forward declaration
There was a problem hiding this comment.
Oh I see what you mean: if you'd like to support this, sure.
| bool function = false; | ||
| bool import = false; | ||
| bool used = false; | ||
| bool softUsed = false; |
There was a problem hiding this comment.
Maybe usedAsRecursiveCall instead?
There was a problem hiding this comment.
i've updated it to usedOutsideSelf and usedRecursively respectively if that's alright.
| Location location; | ||
| bool function; | ||
| bool used; | ||
| unsigned int scopeDepth = 0; |
There was a problem hiding this comment.
You might need scope depth for globals?
There was a problem hiding this comment.
i think i'd prefer to keep the global handling simple if there aren't any apparent issues with it
i flagged it |
|
idk why it auto requested review from vegorov sry that may have been a misclick or something? dunno EDIT: oh theres a new CODEOWNERS in the repo thats kinda cool |
lmfao |
| end | ||
| )"); | ||
|
|
||
| REQUIRE(0 == result.warnings.size()); |
There was a problem hiding this comment.
this test case was accidentally duplicated / incomplete. whoops
hgoldstein
left a comment
There was a problem hiding this comment.
This still looks like a bunch of the changes are not flagged? I would also advise against mixing refactors like renaming variables or providing defaults to struct members with other changes.
I'll look over it again, i guess i was a bit sleep deprived at the time
can you point to a specific commit hash? the three commits since your last review split it up (there was a dedicated commit for the renames) |
took a look now. there are two places where i think adding the flag in other places would obscure the control flow a bit but i can do that if it's wanted |
hgoldstein
left a comment
There was a problem hiding this comment.
i think adding the flag in other places would obscure the control flow a bit but i can do that if it's wanted
When we flag code, we try to make sure that the code path is exactly the same when the flag has been set to off. There are some cases where we fudge things slightly, but it's meant to be extremely conservative. For example, even adding fields to LintLocalHygiene::Local like you've done is technically "unflagged." This maps to our internal process for gauging risk: things that are entirely flagged are minimally risky, less likely to cause issues on rollout, require extensive QA, etc.
That's part of why I ask not to mix refactoring and other changes. Mixing the two makes it really hard to ensure that the flag-off path is exactly the same.
| bool import = false; | ||
| bool usedOutsideSelf = false; | ||
| bool usedRecursively = false; | ||
| bool arg = false; |
There was a problem hiding this comment.
This is what I mean by "don't mix refactoring and other changes." Optimally this would be:
AstNode* defined = nullptr;
bool function;
bool import;
bool used;
bool arg;
bool usedRecursively = false;
unsigned int scopeDepth = 0;- Only make the changes you need, don't mix in renaming fields or adding defaults.
- Just add struct fields at the end rather than also changing the order of fields.
There was a problem hiding this comment.
Just add struct fields at the end rather than also changing the order of fields
sorry, ocd probably. i'll try to do that in the future
Only make the changes you need, don't mix in renaming fields or adding defaults.
the defaults were added aggressively here after i experienced a bug w uninitialized values:
the renames were done here, in their own dedicated commit:
are you suggesting that my changes as a whole shouldn't rename things in the linter? if so, that seems silly - the previous names didn't make as much sense with new fields added, so i changed them
EDIT: silly me, i missed part of the review comment when i wrote that
| l.function = true; | ||
|
|
||
| return true; | ||
| l.scopeDepth++; |
There was a problem hiding this comment.
Oh I see what you mean: if you'd like to support this, sure.
that makes complete sense, yeah. i'll adjust some things to make the original code more clearly specialized with the flag. Surely keeping the off-path the same isn't entirely realistic all the time though, is it? i feel like to do it properly i'd need to make a second copy of every field which i use differently |
It's not, no. Any time you're adding a field to an existing struct, for example, there's a mild risk. The one the Luau team has run into is that even "entirely flagged" blocks in function calls have an effect on the stack size for that function. If you look in |
that's terrifying and fascinating, i can't imagine what kinds of niche failure cases y'all have hit. my approach to flagging this was a bit of a shortcut and i knew it. it can be easy to lose sight of why it's important and view stuff like that as a chore that you can get away with sweeping under the rug, but seeing a team that cares is a breath of fresh air in a world full of LGTMs and LLMs. i'll do my best to match that patience, care, and attention to detail with contributions going forward. |