From be9acc22570e39db920af8acaa822c55ad3cd8f9 Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 15:11:47 +0300 Subject: [PATCH 1/8] Rework value and closure propagation Instead of resolving closure accesses to main assignments when closure is live at an item at which main assignment propagation stops, do it when when closure reaches the assignment or assignment reaches the closure. Ref #126. Instead of cross-resolving all assignments and accesses in all closures, track what closures reach what other closures, and cross-resolve only those pairs. Instead of spelling `propagation` with three `o`s, spell it correctly. --- src/luacheck/analyze.lua | 263 +++++++++++++++++++++------------------ 1 file changed, 139 insertions(+), 124 deletions(-) diff --git a/src/luacheck/analyze.lua b/src/luacheck/analyze.lua index 91fc41e4..37723c62 100644 --- a/src/luacheck/analyze.lua +++ b/src/luacheck/analyze.lua @@ -1,6 +1,35 @@ local core_utils = require "luacheck.core_utils" local utils = require "luacheck.utils" +-- The main part of analysis is connecting assignments to locals or upvalues +-- with accesses that may use the assigned value. +-- Accesses and assignments are split into two groups based on whether they happen +-- in the closure that defines subject local variable (main assignment, main access) +-- or in some nested closure (closure assignment, closure access). +-- To avoid false positives, it's assumed that a closure may be called at any point +-- starting from expression that creates it. +-- Additionally, all operations on upvalues are considered in bulk, as in, +-- when a closure is called, it's assumed that any subset of its upvalue assignments +-- and accesses may happen, in any order. + +-- Assignments and accesses are connected based on whether they can reach each other. +-- A main assignment is connected with a main access when the assignment can reach the access. +-- A main assignment is connected with a closure access when the assignment can reach the closure creation +-- or the closure creation can reach the assignment. +-- A closure assignment is connected with a main access when the closure creation can reach the access. +-- A closure assignment is connected with a closure access when either closure creation can reach the other one. + +-- To determine what flow graph nodes an assignment or a closure creation can reach, +-- they are independently propagated along the graph. +-- Closure creation propagation is not bounded. +-- Main assignment propagation is bounded by entrance and exit conditions for each reached flow graph node. +-- Entrance condition checks that target local variable is still in scope. If entrance condition fails, +-- nothing in the node can refer to the variable, and the scope can't be reentered later. +-- So, in this case, assignment does not reach the node, and propagation does not continue. +-- Exit condition checks that target local variable is not overwritten by an assignment in the node. +-- If it fails, the assignment still reaches the node (because all accesses in a node are evaluated before any +-- assignments take effect), but propagation does not continue. + local function register_value(values_per_var, var, value) if not values_per_var[var] then values_per_var[var] = {} @@ -9,9 +38,11 @@ local function register_value(values_per_var, var, value) table.insert(values_per_var[var], value) end -local function add_resolution(line, item, var, value, mutation) +-- Called when assignment of `value` is connected to an access. +-- `item` contains the access, and `line` contains the item. +local function add_resolution(line, item, var, value, is_mutation) register_value(item.used_values, var, value) - value[mutation and "mutated" or "used"] = true + value[is_mutation and "mutated" or "used"] = true value.using_lines[line] = true if value.secondaries then @@ -19,182 +50,166 @@ local function add_resolution(line, item, var, value, mutation) end end +-- Connects accesses in given items array with an assignment of `value`. +-- `items` may be `nil` instead of empty. +local function add_resolutions(line, items, var, value, is_mutation) + if not items then + return + end + + for _, item in ipairs(items) do + add_resolution(line, item, var, value, is_mutation) + end +end + local function in_scope(var, index) return (var.scope_start <= index) and (index <= var.scope_end) end --- Called when value of var is live at an item, maybe several times. --- Registers value as live where variable is accessed or liveness propogation stops. --- Stops when out of scope of variable, at another assignment to it or at an item --- encountered already. --- When stopping at a visited item, only save value if the item is in the current stack --- of items, i.e. when propogation followed some path from it to previous item -local function value_propogation_callback(line, stack, index, item, visited, var, value) - if not item then - -- Value reach end of line, so even if it's overwritten by a single assignment it's - -- not dominated by it. +-- Called when main assignment propagation reaches a line item. +local function main_assignment_propagation_callback(line, index, item, var, value) + -- Check entrance condition. + -- TODO: check if `not item` can be removed, dummy tail index should always be out of scope. + if not item or not in_scope(var, index) then + -- Assignment reaches the end of variable scope, so it can't be dominated by any assignment. value.overwriting_item = false - register_value(line.last_live_values, var, value) return true end - if not visited[index] then - if item.accesses and item.accesses[var] then - add_resolution(line, item, var, value) - end + -- Assignment reaches this item, apply its effect. - if item.mutations and item.mutations[var] then - add_resolution(line, item, var, value, true) - end + -- Accesses (and mutations) of the variable can resolve to reaching assignment. + if item.accesses and item.accesses[var] then + add_resolution(line, item, var, value) end - local is_overwritten = item.set_variables and item.set_variables[var] - local out_of_scope = not in_scope(var, index) - local stop_and_save = not visited[index] and (out_of_scope or is_overwritten) - - if stack[index] or stop_and_save then - if is_overwritten then - if value.overwriting_item ~= false then - if value.overwriting_item and value.overwriting_item ~= item then - value.overwriting_item = false - else - value.overwriting_item = item - end - end - elseif out_of_scope then - -- Value reach end of scope, so even if it's overwritten by a single assignment it's - -- not dominated by it. - value.overwriting_item = false - end + if item.mutations and item.mutations[var] then + add_resolution(line, item, var, value, true) + end - if not item.live_values then - item.live_values = {} + -- Accesses (and mutations) of the variable inside closures created in this item + -- can resolve to reaching assignment. + if item.lines then + for _, created_line in ipairs(item.lines) do + add_resolutions(created_line, created_line.accessed_upvalues[var], var, value) + add_resolutions(created_line, created_line.mutated_upvalues[var], var, value, true) end - - register_value(item.live_values, var, value) - return true end - if visited[index] then + -- Check exit condition. + if item.set_variables and item.set_variables[var] then + if value.overwriting_item ~= false then + if value.overwriting_item and value.overwriting_item ~= item then + value.overwriting_item = false + else + value.overwriting_item = item + end + end + return true end - - visited[index] = true end --- For each node accessing variables, adds table {var = {values}} to field `used_values`. --- A pair `var = {values}` in this table means that accessed local variable `var` can contain one of values `values`. --- Values that can be accessed locally are marked as used. -local function propogate_values(line) - -- {var = values} live at the end of line. - line.last_live_values = {} - - -- It is not very clever to simply propogate every single assigned value. - -- Fortunately, performance hit seems small (can be compenstated by inlining a few functions in lexer). +-- Connects main assignments with main accesses and closure accesses in reachable closures. +-- TODO: overwriting_item description +local function propagate_main_assignments(line) for i, item in ipairs(line.items) do if item.set_variables then for var, value in pairs(item.set_variables) do if var.line == line then - -- Values are only live at the item after assignment. - core_utils.walk_line(line, i + 1, value_propogation_callback, {}, var, value) + -- Assignments are not live at their own item, because assignments take effect only after all accesses + -- are evaluated. Items with assignments can't be jumps, so they have a single following item + -- with incremented index. + core_utils.walk_line_once(line, {}, i + 1, main_assignment_propagation_callback, var, value) end end end end end --- Called when closure (subline) is live at index. --- Updates variable resolution: --- When a closure accessing upvalue is live at item where a value of the variable is live, --- the access can resolve to the value. --- When a closure setting upvalue is live at item where the variable is accessed, --- the access can resolve to the value. --- Live values are only stored when their liveness ends. However, as closure propogation is unrestricted, --- if there is an intermediate item where value is factually live and closure is live, closure will at some --- point be propogated to where value liveness ends and is stored as live. --- (Chances that I will understand this comment six months later: non-existent) -local function closure_propogation_callback(line, _, item, subline) - local live_values +-- Called when closure creation propagation reaches a line item. +local function closure_creation_propagation_callback(line, _, item, propagated_line) if not item then - live_values = line.last_live_values - else - live_values = item.live_values + return true end - if live_values then - for _, var_map in ipairs({subline.accessed_upvalues, subline.mutated_upvalues}) do - for var, accessing_items in pairs(var_map) do - if var.line == line then - if live_values[var] then - for _, accessing_item in ipairs(accessing_items) do - for _, value in ipairs(live_values[var]) do - add_resolution(subline, accessing_item, var, value, var_map == subline.mutated_upvalues) - end - end - end + -- Closure creation reaches this item, apply its effects. + -- TODO: refactor the following using some helpers. + + -- Accesses (and mutations) of upvalues in the propagated closure + -- can resolve to assignments in the item. + if item.set_variables then + for var, value in pairs(item.set_variables) do + add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], var, value) + add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], var, value, true) + end + end + + -- Accesses (and mutations) of upvalues in the propagated closure + -- can resolve to assignments in closures created in the item. + if item.lines then + for _, created_line in ipairs(item.lines) do + for var, setting_items in pairs(created_line.set_upvalues) do + for _, setting_item in ipairs(setting_items) do + add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], + var, setting_item.set_variables[var]) + add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], + var, setting_item.set_variables[var], true) end end end end - if not item then - return true - end + -- Accesses (and mutations) of locals in the item can resolve + -- to assignments in the propagated closure. + for var, setting_items in pairs(propagated_line.set_upvalues) do + if item.accesses and item.accesses[var] then + for _, setting_item in ipairs(setting_items) do + add_resolution(line, item, var, setting_item.set_variables[var]) + end + end - for _, action_key in ipairs({"accesses", "mutations"}) do - local item_var_map = item[action_key] + if item.mutations and item.mutations[var] then + for _, setting_item in ipairs(setting_items) do + add_resolution(line, item, var, setting_item.set_variables[var], true) + end + end + end - if item_var_map then - for var, setting_items in pairs(subline.set_upvalues) do - if var.line == line then - if item_var_map[var] then - for _, setting_item in ipairs(setting_items) do - add_resolution(line, item, var, setting_item.set_variables[var], action_key == "mutations") - end - end + -- Accesses (and mutations) of upvalues in closures created in the item + -- can resolve to assignments in the propagated closure. + if item.lines then + for _, created_line in ipairs(item.lines) do + for var, setting_items in pairs(propagated_line.set_upvalues) do + for _, setting_item in ipairs(setting_items) do + add_resolutions(created_line, created_line.accessed_upvalues[var], + var, setting_item.set_variables[var]) + add_resolutions(created_line, created_line.mutated_upvalues[var], + var, setting_item.set_variables[var], true) end end end end end --- Updates variable resolution to account for closures and upvalues. -local function propogate_closures(line) +-- Connects main assignments with closure accesses in reaching closures. +-- Connects closure assignments with main accesses and with closure accesses in reachable closures. +-- Connects closure accesses with closure assignments in reachable closures. +local function propagate_closure_creations(line) for i, item in ipairs(line.items) do if item.lines then - for _, subline in ipairs(item.lines) do - -- Closures are considered live at the item they are created. - core_utils.walk_line_once(line, {}, i, closure_propogation_callback, subline) - end - end - end - - -- It is assumed that all closures are live at the end of the line. - -- Therefore, all accesses and sets inside closures can resolve to each other. - for _, subline in ipairs(line.lines) do - for _, var_map in ipairs({subline.accessed_upvalues, subline.mutated_upvalues}) do - for var, accessing_items in pairs(var_map) do - if var.line == line then - for _, accessing_item in ipairs(accessing_items) do - for _, another_subline in ipairs(line.lines) do - if another_subline.set_upvalues[var] then - for _, setting_item in ipairs(another_subline.set_upvalues[var]) do - add_resolution(subline, accessing_item, var, - setting_item.set_variables[var], var_map == subline.mutated_upvalues) - end - end - end - end - end + for _, created_line in ipairs(item.lines) do + -- Closures are live at the item they are created, as they can be called immediately. + core_utils.walk_line_once(line, {}, i, closure_creation_propagation_callback, created_line) end end end end local function analyze_line(line) - propogate_values(line) - propogate_closures(line) + propagate_main_assignments(line) + propagate_closure_creations(line) end local function is_function_var(var) From 04cfc6870820d0c09c5ff8b16c23c560fc05d6a2 Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 16:45:18 +0300 Subject: [PATCH 2/8] Refactor closure cross-resolving Still some very similar code for various resolution purposes, but good enough. --- src/luacheck/analyze.lua | 46 +++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/luacheck/analyze.lua b/src/luacheck/analyze.lua index 37723c62..cc71da5b 100644 --- a/src/luacheck/analyze.lua +++ b/src/luacheck/analyze.lua @@ -62,6 +62,19 @@ local function add_resolutions(line, items, var, value, is_mutation) end end +-- Connects all accesses (and mutations) in `access_line` with corresponding +-- assignments in `set_line`. +local function cross_resolve_closures(access_line, set_line) + for var, setting_items in pairs(set_line.set_upvalues) do + for _, setting_item in ipairs(setting_items) do + add_resolutions(access_line, access_line.accessed_upvalues[var], + var, setting_item.set_variables[var]) + add_resolutions(access_line, access_line.mutated_upvalues[var], + var, setting_item.set_variables[var], true) + end + end +end + local function in_scope(var, index) return (var.scope_start <= index) and (index <= var.scope_end) end @@ -135,7 +148,6 @@ local function closure_creation_propagation_callback(line, _, item, propagated_l end -- Closure creation reaches this item, apply its effects. - -- TODO: refactor the following using some helpers. -- Accesses (and mutations) of upvalues in the propagated closure -- can resolve to assignments in the item. @@ -146,18 +158,15 @@ local function closure_creation_propagation_callback(line, _, item, propagated_l end end - -- Accesses (and mutations) of upvalues in the propagated closure - -- can resolve to assignments in closures created in the item. if item.lines then for _, created_line in ipairs(item.lines) do - for var, setting_items in pairs(created_line.set_upvalues) do - for _, setting_item in ipairs(setting_items) do - add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], - var, setting_item.set_variables[var]) - add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], - var, setting_item.set_variables[var], true) - end - end + -- Accesses (and mutations) of upvalues in the propagated closure + -- can resolve to assignments in closures created in the item. + cross_resolve_closures(propagated_line, created_line) + + -- Accesses (and mutations) of upvalues in closures created in the item + -- can resolve to assignments in the propagated closure. + cross_resolve_closures(created_line, propagated_line) end end @@ -176,21 +185,6 @@ local function closure_creation_propagation_callback(line, _, item, propagated_l end end end - - -- Accesses (and mutations) of upvalues in closures created in the item - -- can resolve to assignments in the propagated closure. - if item.lines then - for _, created_line in ipairs(item.lines) do - for var, setting_items in pairs(propagated_line.set_upvalues) do - for _, setting_item in ipairs(setting_items) do - add_resolutions(created_line, created_line.accessed_upvalues[var], - var, setting_item.set_variables[var]) - add_resolutions(created_line, created_line.mutated_upvalues[var], - var, setting_item.set_variables[var], true) - end - end - end - end end -- Connects main assignments with closure accesses in reaching closures. From c89ce4db515500e2cbdd40a2f8f95f372ed7a76f Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 17:22:46 +0300 Subject: [PATCH 3/8] Remove core_utils.walk_line, use recursion for walk_line_once Ability to walk flow graph without stopping at already visited items is no longer needed. Remove walk_line, rewrite walk_line_once using recursion (should be a bit faster), rename walk_line_once -> walk_line, as it used to be when it was added. --- src/luacheck/analyze.lua | 4 +-- src/luacheck/core_utils.lua | 60 ++++++++++------------------------- src/luacheck/reachability.lua | 4 +-- 3 files changed, 21 insertions(+), 47 deletions(-) diff --git a/src/luacheck/analyze.lua b/src/luacheck/analyze.lua index cc71da5b..b12fb9fc 100644 --- a/src/luacheck/analyze.lua +++ b/src/luacheck/analyze.lua @@ -133,7 +133,7 @@ local function propagate_main_assignments(line) -- Assignments are not live at their own item, because assignments take effect only after all accesses -- are evaluated. Items with assignments can't be jumps, so they have a single following item -- with incremented index. - core_utils.walk_line_once(line, {}, i + 1, main_assignment_propagation_callback, var, value) + core_utils.walk_line(line, {}, i + 1, main_assignment_propagation_callback, var, value) end end end @@ -195,7 +195,7 @@ local function propagate_closure_creations(line) if item.lines then for _, created_line in ipairs(item.lines) do -- Closures are live at the item they are created, as they can be called immediately. - core_utils.walk_line_once(line, {}, i, closure_creation_propagation_callback, created_line) + core_utils.walk_line(line, {}, i, closure_creation_propagation_callback, created_line) end end end diff --git a/src/luacheck/core_utils.lua b/src/luacheck/core_utils.lua index 3460c2f0..fc77d23c 100644 --- a/src/luacheck/core_utils.lua +++ b/src/luacheck/core_utils.lua @@ -1,56 +1,30 @@ local core_utils = {} --- Calls callback with line, stack_set, index, item, ... for each item reachable from starting item. --- `stack_set` is a set of indices of items in current propogation path from root, excluding current item. +-- Calls callback with line, index, item, ... for each item reachable from starting item. +-- `visited` is a set of already visited indexes. -- Callback can return true to stop walking from current item. -function core_utils.walk_line(line, index, callback, ...) - local stack = {} - local stack_set = {} - local backlog = {} - local level = 0 - - while index do - local item = line.items[index] +function core_utils.walk_line(line, visited, index, callback, ...) + if visited[index] then + return + end - if not callback(line, stack_set, index, item, ...) and item then - level = level + 1 - stack[level] = index - stack_set[index] = true + visited[index] = true - if item.tag == "Jump" then - index = item.to - elseif item.tag == "Cjump" then - backlog[level] = index + 1 - index = item.to - else - index = index + 1 - end - else - while level > 0 and not backlog[level] do - stack_set[stack[level]] = nil - level = level - 1 - end + local item = line.items[index] - index = backlog[level] - backlog[level] = nil - end + if callback(line, index, item, ...) then + return end -end -local function once_per_item_callback_adapter(line, _, index, item, visited, callback, ...) - if visited[index] then - return true + if not item then + return + elseif item.tag == "Jump" then + return core_utils.walk_line(line, visited, item.to, callback, ...) + elseif item.tag == "Cjump" then + core_utils.walk_line(line, visited, item.to, callback, ...) end - visited[index] = true - return callback(line, index, item, ...) -end - --- Calls callback with line, index, item, ... for each item reachable from starting item once. --- `visited` is a set of already visited indexes. --- Callback can return true to stop walking from current item. -function core_utils.walk_line_once(line, visited, index, callback, ...) - return core_utils.walk_line(line, index, once_per_item_callback_adapter, visited, callback, ...) + return core_utils.walk_line(line, visited, index + 1, callback, ...) end -- Given a "global set" warning, return whether it is an implicit definition. diff --git a/src/luacheck/reachability.lua b/src/luacheck/reachability.lua index 8a6af740..3de62b9b 100644 --- a/src/luacheck/reachability.lua +++ b/src/luacheck/reachability.lua @@ -44,13 +44,13 @@ end -- Emits warnings: unreachable code, uninitialized access. function reachability(chstate, line, nested) local reachable_indexes = {} - core_utils.walk_line_once(line, reachable_indexes, 1, reachability_callback, chstate, nested) + core_utils.walk_line(line, reachable_indexes, 1, reachability_callback, chstate, nested) for i, item in ipairs(line.items) do if not reachable_indexes[i] then if item.location then chstate:warn_unreachable(item.location, item.loop_end, item.token) - core_utils.walk_line_once(line, reachable_indexes, i, noop_callback) + core_utils.walk_line(line, reachable_indexes, i, noop_callback) end end end From a4f34407850156d2ea833a3bbab7c4cd303c9c2f Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 17:27:10 +0300 Subject: [PATCH 4/8] Increment cache version --- src/luacheck/cache.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/luacheck/cache.lua b/src/luacheck/cache.lua index 4ae6d68a..9222afa2 100644 --- a/src/luacheck/cache.lua +++ b/src/luacheck/cache.lua @@ -9,7 +9,7 @@ local cache = {} -- third is check result in lua table format. -- String fields are compressed into array indexes. -cache.format_version = 21 +cache.format_version = 22 local option_fields = { "ignore", "std", "globals", "unused_args", "self", "compat", "global", "unused", "redefined", From c2cdeb3881bf698f458982819e945f8a7c6bb65d Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 18:21:02 +0300 Subject: [PATCH 5/8] Add description of value.overwriting_item field --- src/luacheck/analyze.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/luacheck/analyze.lua b/src/luacheck/analyze.lua index b12fb9fc..fbf97f13 100644 --- a/src/luacheck/analyze.lua +++ b/src/luacheck/analyze.lua @@ -124,7 +124,10 @@ local function main_assignment_propagation_callback(line, index, item, var, valu end -- Connects main assignments with main accesses and closure accesses in reachable closures. --- TODO: overwriting_item description +-- Additionally, sets `overwriting_item` field of values to an item with an assignment overwriting +-- the value, but only if the overwriting is not avoidable (i.e. it's impossible to reach end of function +-- from the first assignment without going through the second one). Otherwise value of the field may be +-- `false` or `nil`. local function propagate_main_assignments(line) for i, item in ipairs(line.items) do if item.set_variables then From 9178176723052de4c777cdfa3088bba7d24e99a9 Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 18:26:09 +0300 Subject: [PATCH 6/8] Remove a useless check in analyze.lua --- src/luacheck/analyze.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/luacheck/analyze.lua b/src/luacheck/analyze.lua index fbf97f13..f7a84c96 100644 --- a/src/luacheck/analyze.lua +++ b/src/luacheck/analyze.lua @@ -82,8 +82,7 @@ end -- Called when main assignment propagation reaches a line item. local function main_assignment_propagation_callback(line, index, item, var, value) -- Check entrance condition. - -- TODO: check if `not item` can be removed, dummy tail index should always be out of scope. - if not item or not in_scope(var, index) then + if not in_scope(var, index) then -- Assignment reaches the end of variable scope, so it can't be dominated by any assignment. value.overwriting_item = false return true From 929bf70b7737dffb80f7ab6f3cc444ea705d2353 Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 18:37:58 +0300 Subject: [PATCH 7/8] Add tests for #126 fix --- spec/check_spec.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/check_spec.lua b/spec/check_spec.lua index 40f87345..1cacd7af 100644 --- a/spec/check_spec.lua +++ b/spec/check_spec.lua @@ -536,6 +536,36 @@ end ]]) end) + it("detects unused local value referred to from closure in incompatible branch", function() + assert.same({ + {code = "311", name = "a", line = 4, column = 4, end_column = 4}, + {code = "321", name = "a", line = 6, column = 28, end_column = 28} + }, check[[ +local a + +if (...)() then + a = 1 +else + (...)(function() return a end) +end +]]) + end) + + it("detects unused upvalue value referred to from closure in incompatible branch", function() + assert.same({ + {code = "311", name = "a", line = 4, column = 21, end_column = 21}, + {code = "321", name = "a", line = 6, column = 28, end_column = 28} + }, check[[ +local a + +if (...)() then + (...)(function() a = 1 end) +else + (...)(function() return a end) +end +]]) + end) + it("handles upvalues before infinite loops", function() assert.same({ {code = "221", name = "x", line = 1, column = 7, end_column = 7}, From 5770e027bf235ec0f7900995a1cc5638a5d961b1 Mon Sep 17 00:00:00 2001 From: Peter Melnichenko Date: Sat, 9 Sep 2017 18:41:07 +0300 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42fcf64..117a7076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Fixes * Added missing definition of `ngx.ERROR` constant to `ngx_lua` std (#123). +* Fixed unused values and initialized accesses not being reported when the + access is in a closure defined in code path incompatible with the value + assignment (#126). ## 0.21.0 (2017-09-04)