Skip to content

Commit

Permalink
Merge pull request #66 from a2/a2/compound-operators
Browse files Browse the repository at this point in the history
Add support for compound operators
  • Loading branch information
arichard4 committed Dec 18, 2022
2 parents 54b19b2 + e6fd44f commit 153de60
Show file tree
Hide file tree
Showing 19 changed files with 197 additions and 16 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Use the Luacheck issue tracker on GitHub to submit bugs, suggestions and questio

## Building and testing

After the Luacheck repo is cloned and changes are made, run `luarocks make` (using `sudo` if necessary) from its root directory to install dev version of Luacheck. To run Luacheck using sources in current directory without installing it, run `lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' bin/luacheck.lua ...`. To test Luacheck, ensure that you have [busted](http://olivinelabs.com/busted/) and [luautf8](https://github.com/starwing/luautf8) installed and run `busted`.
After the Luacheck repo is cloned and changes are made, run `luarocks make` (using `sudo` if necessary) from its root directory to install dev version of Luacheck. To run Luacheck using sources in current directory without installing it, run `lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' bin/luacheck.lua ...`. To test Luacheck, ensure that you have [busted](http://olivinelabs.com/busted/), [luautf8](https://github.com/starwing/luautf8), and [luasocket](https://github.com/lunarmodules/luasocket) installed and run `busted`.

## Docker

Expand Down
1 change: 1 addition & 0 deletions docsrc/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Option Meaning
``--ignore | -i <patt> [<patt>] ...`` Filter out warnings matching patterns.
``--enable | -e <patt> [<patt>] ...`` Do not filter out warnings matching patterns.
``--only | -o <patt> [<patt>] ...`` Filter out warnings not matching patterns.
``--operators <patt> [<patt>] ...`` Allow compound operators matching patterns. (Multiple assignment not supported, as this is specifically for the Playdate SDK.)
``--config <config>`` Path to custom configuration file (default: ``.luacheckrc``).
``--no-config`` Do not look up custom configuration file.
``--default-config <config>`` Default path to custom configuration file, to be used if ``--[no-]config`` is not used and ``.luacheckrc`` is not found.
Expand Down
1 change: 1 addition & 0 deletions docsrc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Option Type Default v
``read_globals`` Array of strings or field definition map ``{}``
``new_read_globals`` Array of strings or field definition map (Do not overwrite)
``not_globals`` Array of strings ``{}``
``operators`` Array of strings ``{}``
``compat`` Boolean ``false``
``allow_defined`` Boolean ``false``
``allow_defined_top`` Boolean ``false``
Expand Down
1 change: 1 addition & 0 deletions docsrc/warnings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Code Description
021 An invalid inline option.
022 An unpaired inline push directive.
023 An unpaired inline pop directive.
033 Invalid use of a compound operator. (Lua doesn't support compound operator by default; if using an extension that does, please set the operators option.)
111 Setting an undefined global variable.
112 Mutating an undefined global variable.
113 Accessing an undefined global variable.
Expand Down
1 change: 1 addition & 0 deletions luacheck-dev-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ build = {
["luacheck.serializer"] = "src/luacheck/serializer.lua",
["luacheck.stages"] = "src/luacheck/stages/init.lua",
["luacheck.stages.detect_bad_whitespace"] = "src/luacheck/stages/detect_bad_whitespace.lua",
["luacheck.stages.detect_compound_operators"] = "src/luacheck/stages/detect_compound_operators.lua",
["luacheck.stages.detect_cyclomatic_complexity"] = "src/luacheck/stages/detect_cyclomatic_complexity.lua",
["luacheck.stages.detect_empty_blocks"] = "src/luacheck/stages/detect_empty_blocks.lua",
["luacheck.stages.detect_empty_statements"] = "src/luacheck/stages/detect_empty_statements.lua",
Expand Down
41 changes: 37 additions & 4 deletions spec/cli_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,36 @@ Total: 5 warnings / 0 errors in 1 file
]], get_output "--std lua51+lua52+lua53 spec/samples/bad_code.lua --no-config")
end)

it("raises critical errors on config without additional operators", function()
assert.equal([[Checking spec/samples/compound_operators.lua 4 errors
spec/samples/compound_operators.lua:2:1: assignment uses compound operator +=
spec/samples/compound_operators.lua:3:1: assignment uses compound operator -=
spec/samples/compound_operators.lua:4:1: assignment uses compound operator *=
spec/samples/compound_operators.lua:5:1: assignment uses compound operator /=
Total: 0 warnings / 4 errors in 1 file
]], get_output "spec/samples/compound_operators.lua --no-config")
end)

it("raises critical errors for unfiltered additional operators", function()
assert.equal([[Checking spec/samples/compound_operators.lua 3 errors
spec/samples/compound_operators.lua:3:1: assignment uses compound operator -=
spec/samples/compound_operators.lua:4:1: assignment uses compound operator *=
spec/samples/compound_operators.lua:5:1: assignment uses compound operator /=
Total: 0 warnings / 3 errors in 1 file
]], get_output "spec/samples/compound_operators.lua --no-config --operators +=")
end)

it("allows to define allowed compound operators", function()
assert.equal([[Checking spec/samples/compound_operators.lua OK
Total: 0 warnings / 0 errors in 1 file
]], get_output "spec/samples/compound_operators.lua --config=spec/configs/compound_operators_config.luacheckrc")
end)

it("allows to ignore some variables", function()
assert.equal([[
Checking spec/samples/bad_code.lua 3 warnings
Expand Down Expand Up @@ -993,7 +1023,7 @@ spec/samples/python_code.lua:1:6: (E011) expected '=' near '__future__'
end)

it("expands folders", function()
assert.matches("^Total: %d+ warnings / %d+ errors in 26 files\n$", get_output "spec/samples -qqq --no-config --exclude-files spec/samples/global_fields.lua")
assert.matches("^Total: %d+ warnings / %d+ errors in 27 files\n$", get_output "spec/samples -qqq --no-config --exclude-files spec/samples/global_fields.lua")
end)

it("uses --include-files when expanding folders", function()
Expand Down Expand Up @@ -1205,6 +1235,7 @@ Codes: true
assert.equal(([[
Checking spec/samples/argparse-0.2.0.lua 9 warnings
Checking spec/samples/compat.lua 4 warnings
Checking spec/samples/compound_operators.lua 4 errors
Checking spec/samples/custom_std_inline_options.lua 3 warnings / 1 error
Checking spec/samples/global_inline_options.lua 3 warnings
Checking spec/samples/globals.lua 2 warnings
Expand All @@ -1221,7 +1252,7 @@ Checking spec/samples/unused_secondaries.lua 4 warnings
Checking spec/samples/utf8.lua 4 warnings
Checking spec/samples/utf8_error.lua 1 error
Total: 73 warnings / 5 errors in 19 files
Total: 73 warnings / 9 errors in 20 files
]]):gsub("(spec/samples)/", "%1"..package.config:sub(1, 1)),
get_output "spec/samples --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files spec/samples/global_fields.lua")
end)
Expand All @@ -1230,6 +1261,7 @@ Total: 73 warnings / 5 errors in 19 files
assert.equal([[
Checking argparse-0.2.0.lua 9 warnings
Checking compat.lua 4 warnings
Checking compound_operators.lua 4 errors
Checking custom_std_inline_options.lua 3 warnings / 1 error
Checking global_inline_options.lua 3 warnings
Checking globals.lua 2 warnings
Expand All @@ -1246,14 +1278,15 @@ Checking unused_secondaries.lua 4 warnings
Checking utf8.lua 4 warnings
Checking utf8_error.lua 1 error
Total: 73 warnings / 5 errors in 19 files
Total: 73 warnings / 9 errors in 20 files
]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua", "spec/samples/"))
end)

it("combines excluded files from config and cli", function()
assert.equal([[
Checking argparse-0.2.0.lua 9 warnings
Checking compat.lua 4 warnings
Checking compound_operators.lua 4 errors
Checking custom_std_inline_options.lua 3 warnings / 1 error
Checking global_inline_options.lua 3 warnings
Checking globals.lua 2 warnings
Expand All @@ -1268,7 +1301,7 @@ Checking unused_secondaries.lua 4 warnings
Checking utf8.lua 4 warnings
Checking utf8_error.lua 1 error
Total: 65 warnings / 5 errors in 17 files
Total: 65 warnings / 9 errors in 18 files
]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua --exclude-files " .. quote("./read*"), "spec/samples/"))
end)

Expand Down
1 change: 1 addition & 0 deletions spec/configs/compound_operators_config.luacheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
operators = {"+=", "-=", "*=", "/="}
6 changes: 6 additions & 0 deletions spec/samples/compound_operators.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
local i = 0
i += 10
i -= 5
i *= 2
i /= 5
return i
2 changes: 1 addition & 1 deletion src/luacheck/builtin_standards/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ builtin_standards.luacheckrc = {
"allow_defined_top", "module", "globals", "read_globals", "new_globals", "new_read_globals", "not_globals",
"ignore", "enable", "only", "std", "max_line_length", "max_code_line_length", "max_string_line_length",
"max_comment_line_length", "max_cyclomatic_complexity", "quiet", "color", "codes", "ranges", "formatter",
"cache", "jobs", "files", "stds", "exclude_files", "include_files"
"cache", "jobs", "files", "stds", "exclude_files", "include_files", "operators"
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/luacheck/filter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ local function passes_filter(normalized_options, warning)
end

warning.max_complexity = max_complexity
elseif warning.code == "033" then
local operators = normalized_options.operators or {}
for _, op in ipairs(operators) do
if warning.operator == op then
return false
end
end

return true
elseif warning.code:find("^[234]") and warning.name == "_" and not warning.useless then
return false
elseif warning.code:find("^1[14]") then
Expand Down
6 changes: 6 additions & 0 deletions src/luacheck/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ Links:
:action "concat"
:init(nil),
parser:option("--only -o", "Filter out warnings not matching these patterns.")
:args "+"
:count "*"
:argname "<patt>"
:action "concat"
:init(nil),
parser:option("--operators", "Allow compound operators matching patterns")
:args "+"
:count "*"
:argname "<patt>"
Expand Down
26 changes: 24 additions & 2 deletions src/luacheck/options.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ options.variadic_inline_options = {
not_globals = array_of_strings,
ignore = array_of_strings,
enable = array_of_strings,
only = array_of_strings
only = array_of_strings,
}

options.all_options = {
Expand All @@ -79,7 +79,8 @@ options.all_options = {
max_code_line_length = number_or_false,
max_string_line_length = number_or_false,
max_comment_line_length = number_or_false,
max_cyclomatic_complexity = number_or_false
max_cyclomatic_complexity = number_or_false,
operators = array_of_strings
}

utils.update(options.all_options, options.nullary_inline_options)
Expand Down Expand Up @@ -385,6 +386,26 @@ local function normalize_patterns(rules)
return res
end

local function get_operators(opts_stack)
local operators, operatorsMap = nil, nil

for _, opts in ipairs(opts_stack) do
if opts.operators then
operators = operators or {}
operatorsMap = operatorsMap or {}

for _, op in ipairs(opts.operators) do
if not operatorsMap[op] then
table.insert(operators, op)
operatorsMap[op] = true
end
end
end
end

return operators
end

local scalar_options = {
unused_secondaries = true,
self = true,
Expand All @@ -404,6 +425,7 @@ function options.normalize(opts_stack, stds)
local res = {}
stds = stds or builtin_standards
res.std = get_final_std(opts_stack, stds)
res.operators = get_operators(opts_stack)

for option, default in pairs(scalar_options) do
res[option] = get_scalar_opt(opts_stack, option, default)
Expand Down
38 changes: 35 additions & 3 deletions src/luacheck/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,16 @@ local binary_operators = {
["and"] = "and", ["or"] = "or"
}

local compound_operators = {
["+"] = "add", ["-"] = "sub",
["*"] = "mul", ["%"] = "mod",
["^"] = "pow",
["/"] = "div", ["//"] = "idiv",
["&"] = "band", ["|"] = "bor", ["~"] = "bxor",
["<<"] = "shl", [">>"] = "shr",
[".."] = "concat"
}

local left_priorities = {
add = 10, sub = 10,
mul = 11, mod = 11,
Expand Down Expand Up @@ -903,9 +913,31 @@ local function parse_expression_statement(state)
lhs[#lhs + 1] = primary_expression
until not test_and_skip_token(state, ",")

check_and_skip_token(state, "=")
local rhs = parse_expression_list(state)
return new_inner_node(start_range, rhs[#rhs], "Set", {lhs, rhs})
local compound_operator = compound_operators[state.token]
if compound_operator then
-- This is an assignment in the form `lhs op= rhs`.

if #lhs ~= 1 then
-- Multiple lhs values are not valid
parse_error(state, "compound assignment not allowed on tuples near " .. compound_operator .. "=")
end

-- Skip operator.
skip_token(state)
check_and_skip_token(state, "=")

local rhs = parse_expression_list(state)
if #rhs ~= 1 then
parse_error(state, "compound assignment not allowed on tuples near " .. compound_operator .. "=")
end

return new_inner_node(start_range, rhs[1], "OpSet", {compound_operator, lhs[1], rhs[1]})
else
-- This is an assignment in the form `lhs = rhs`.
check_and_skip_token(state, "=")
local rhs = parse_expression_list(state)
return new_inner_node(start_range, rhs[#rhs], "Set", {lhs, rhs})
end
end

local function parse_statement(state)
Expand Down
2 changes: 1 addition & 1 deletion src/luacheck/serializer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local option_fields = {
"unused_secondaries", "allow_defined", "allow_defined_top", "module",
"read_globals", "new_globals", "new_read_globals", "enable", "only", "not_globals",
"max_line_length", "max_code_line_length", "max_string_line_length", "max_comment_line_length",
"max_cyclomatic_complexity"
"max_cyclomatic_complexity", "operators"
}

local function compress_table(t, fields)
Expand Down
34 changes: 34 additions & 0 deletions src/luacheck/stages/detect_compound_operators.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
local core_utils = require "luacheck.core_utils"

local stage = {}

stage.warnings = {
["033"] = {message_format = "assignment uses compound operator {operator}", fields = {"operator"}},
}

local reverse_compound_operators = {
add = "+=",
sub = "-=",
mul = "*=",
mod = "%=",
pow = "^=",
div = "/=",
idiv = "//=",
band = "&=",
bor = "|=",
bxor = "~=",
shl = "<<=",
shr = ">>=",
concat = "..="
}

local function check_node(chstate, node)
local operator = reverse_compound_operators[node[1]]
chstate:warn_range("033", node, {operator = operator})
end

function stage.run(chstate)
core_utils.each_statement(chstate, { "OpSet" }, check_node)
end

return stage
1 change: 1 addition & 0 deletions src/luacheck/stages/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ stages.names = {
"name_functions",
"resolve_locals",
"detect_bad_whitespace",
"detect_compound_operators",
"detect_cyclomatic_complexity",
"detect_empty_blocks",
"detect_empty_statements",
Expand Down
33 changes: 33 additions & 0 deletions src/luacheck/stages/linearize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ local function new_set_item(node)
}
end

local function new_opset_item(node)
return {
tag = "OpSet",
node = node,
lhs = node[2],
rhs = node[3],
accesses = {},
mutations = {},
used_values = {},
lines = {}
}
end

local function is_unpacking(node)
return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke"
end
Expand Down Expand Up @@ -515,6 +528,24 @@ function LinState:emit_stmt_Set(node)
self:emit(item)
end

function LinState:emit_stmt_OpSet(node)
local item = new_opset_item(node)
self:scan_expr(item, node[3])

local lhs = node[2]
if lhs.tag == "Id" then
local var = self:check_var(lhs)

if var then
self:register_upvalue_action(item, var, "set_upvalues")
end
else
assert(lhs.tag == "Index")
self:scan_lhs_index(item, lhs)
end

self:emit(item)
end

function LinState:scan_expr(item, node)
local scanner = self["scan_expr_" .. node.tag]
Expand Down Expand Up @@ -611,6 +642,8 @@ function LinState:scan_expr_Op(item, node)
end
end

LinState.scan_expr_OpSet = LinState.scan_expr_Op

-- Puts tables {var = value} into field `set_variables` of items in line which set values.
-- Registers set values in field `values` of variables.
function LinState:register_set_variables()
Expand Down
Loading

0 comments on commit 153de60

Please sign in to comment.