luacheck-0.13.0/0000755000175000017500000000000012642521554012412 5ustar vsevavsevaluacheck-0.13.0/README.md0000644000175000017500000001617412642521554013702 0ustar vsevavseva# Luacheck [![Build Status](https://travis-ci.org/mpeterv/luacheck.png?branch=master)](https://travis-ci.org/mpeterv/luacheck) [![Windows build status](https://ci.appveyor.com/api/projects/status/pgox2vvelagw1fux/branch/master?svg=true&passingText=Windows%20build%20passing&failingText=Windows%20build%20failing)](https://ci.appveyor.com/project/mpeterv/luacheck/branch/master) [![Coverage Status](https://coveralls.io/repos/mpeterv/luacheck/badge.svg?branch=master)](https://coveralls.io/r/mpeterv/luacheck?branch=master) [![License](http://img.shields.io/badge/License-MIT-brightgreen.svg)](LICENSE) ## Contents * [Overview](#overview) * [Installation](#installation) * [Related projects](#related-projects) * [Documentation](#documentation) * [Development](#development) * [Building and testing](#building-and-testing) * [License](#license) ## Overview Luacheck is a static analyzer and a linter for [Lua](http://www.lua.org). Luacheck detects various issues such as usage of undefined global variables, unused variables and values, accessing uninitialized variables, unreachable code and more. Most aspects of checking are configurable: there are options for defining custom project-related globals, for selecting set of standard globals (version of Lua standard library), for filtering warnings by type and name of related variable, etc. The options can be used on the command line, put into a config or directly into checked files as Lua comments. Luacheck supports checking Lua files using syntax of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0. Luacheck itself is written in Lua and runs on all of mentioned Lua versions. ## Installation The easiest way to install Luacheck is to use [LuaRocks](http://luarocks.org). From your command line run the following command: ```bash $ luarocks install luacheck # prepend with sudo if necessary ``` If it is not possible to install [LuaFileSystem](http://keplerproject.github.io/luafilesystem/) in your environment, use `luarocks install luacheck --deps-mode=none`. For parallel checking Luacheck additionally requires [LuaLanes](https://github.com/LuaLanes/lanes), which can be installed using LuaRocks as well. ### Manual installation For manual installation, only a Lua interpreter binary is required. 1. Download and unpack latest Luacheck release ([.zip](https://github.com/mpeterv/luacheck/archive/0.13.0.zip) [.tar.gz](https://github.com/mpeterv/luacheck/archive/0.13.0.tar.gz)). 2. Run `install.lua ` script using the Lua interpreter. If Lua interpreter is not in `PATH`, invoke it using absolute path. 3. Add `/bin` to PATH or run Luacheck as `/bin/luacheck`. ## Basic usage After Luacheck is installed, run `luacheck` program from the command line. Pass a list of files, [rockspecs](https://github.com/keplerproject/luarocks/wiki/Rockspec-format) or directories (requires LuaFileSystem) to be checked: ``` $ luacheck src extra_file.lua another_file.lua Checking src/good_code.lua OK Checking src/bad_code.lua 3 warnings src/bad_code.lua:3:23: unused variable length argument src/bad_code.lua:7:10: setting non-standard global variable embrace src/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 Checking src/python_code.lua 1 error src/python_code.lua:1:6: expected '=' near '__future__' Checking extra_file.lua 5 warnings extra_file.lua:3:18: unused argument baz extra_file.lua:4:8: unused loop variable i extra_file.lua:13:7: accessing uninitialized variable a extra_file.lua:14:1: value assigned to variable x is unused extra_file.lua:21:7: variable z is never accessed Checking another_file.lua 2 warnings another_file.lua:2:7: unused variable height another_file.lua:3:7: accessing undefined variable heigth Total: 10 warnings / 1 error in 5 files ``` For more info, see [documentation](#documentation). ## Related projects ### Editor support There are a few plugins which allow using Luacheck directly inside an editor, showing warnings inline: * For Vim, [Syntastic](https://github.com/scrooloose/syntastic/) contains [luacheck checker](https://github.com/scrooloose/syntastic/wiki/Lua%3A---luacheck); * For Sublime Text 3 there is [SublimeLinter-luacheck](https://sublime.wbond.net/packages/SublimeLinter-luacheck) which requires [SublimeLinter](http://sublimelinter.readthedocs.org/en/latest/); * For Atom there is [linter-luacheck](https://atom.io/packages/linter-luacheck) which requires [AtomLinter](https://github.com/AtomLinter/Linter); * For Emacs, [Flycheck](http://www.flycheck.org/) contains [luacheck checker](http://www.flycheck.org/manual/latest/Supported-languages.html#Lua); * For Brackets, there is [linter.luacheck](https://github.com/Malcolm3141/brackets-luacheck) extension. If you are a plugin developer, see [recommended way of using Luacheck in a plugin](http://luacheck.readthedocs.org/en/0.13.0/cli.html#stable-interface-for-editor-plugins-and-tools). ### Other projects * [Luacheck bindings for Node.js](https://www.npmjs.com/package/luacheck); * [Luacheck plugin for Gulp](https://www.npmjs.com/package/gulp-luacheck). ## Documentation Documentation is available [online](http://luacheck.readthedocs.org). If Luacheck has been installed using LuaRocks, it can be browsed offline using `luarocks doc luacheck` command. Documentation can be built using [Sphinx](http://sphinx-doc.org/): `sphinx-build docsrc doc`, the files will be found inside `doc/`. ## Development Luacheck is currently in development. The latest released version is 0.13.0. The interface of the `luacheck` module may change between minor releases. The command line interface is fairly stable. Use the Luacheck issue tracker on GitHub to submit bugs, suggestions and questions. Any pull requests are welcome, too. ## Building and testing After the Luacheck repo is cloned and changes are made, run `luarocks make` (optionally prepended with `sudo`) from its root directory to install dev version of Luacheck. To test Luacheck, ensure that you have [busted](http://olivinelabs.com/busted) installed and run `busted`. ## License ``` The MIT License (MIT) Copyright (c) 2014 - 2016 Peter Melnichenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` luacheck-0.13.0/.gitignore0000644000175000017500000000007712642521554014406 0ustar vsevavsevabin/luacheck .luacheckcache luacov.stats.out luacov.report.out luacheck-0.13.0/luacheck-scm-1.rockspec0000644000175000017500000000403312642521554016642 0ustar vsevavsevapackage = "luacheck" version = "scm-1" source = { url = "git+https://github.com/mpeterv/luacheck.git" } description = { summary = "A static analyzer and a linter for Lua", detailed = [[ Luacheck is a command-line tool for linting and static analysis of Lua code. It is able to spot usage of undefined global variables, unused local variables and a few other typical problems within Lua programs. ]], homepage = "https://github.com/mpeterv/luacheck", license = "MIT " } dependencies = { "lua >= 5.1, < 5.4", "luafilesystem >= 1.6.3" } build = { type = "builtin", modules = { luacheck = "src/luacheck/init.lua", ["luacheck.main"] = "src/luacheck/main.lua", ["luacheck.config"] = "src/luacheck/config.lua", ["luacheck.linearize"] = "src/luacheck/linearize.lua", ["luacheck.analyze"] = "src/luacheck/analyze.lua", ["luacheck.reachability"] = "src/luacheck/reachability.lua", ["luacheck.core_utils"] = "src/luacheck/core_utils.lua", ["luacheck.check"] = "src/luacheck/check.lua", ["luacheck.parser"] = "src/luacheck/parser.lua", ["luacheck.lexer"] = "src/luacheck/lexer.lua", ["luacheck.filter"] = "src/luacheck/filter.lua", ["luacheck.options"] = "src/luacheck/options.lua", ["luacheck.inline_options"] = "src/luacheck/inline_options.lua", ["luacheck.stds"] = "src/luacheck/stds.lua", ["luacheck.expand_rockspec"] = "src/luacheck/expand_rockspec.lua", ["luacheck.multithreading"] = "src/luacheck/multithreading.lua", ["luacheck.cache"] = "src/luacheck/cache.lua", ["luacheck.format"] = "src/luacheck/format.lua", ["luacheck.version"] = "src/luacheck/version.lua", ["luacheck.fs"] = "src/luacheck/fs.lua", ["luacheck.globbing"] = "src/luacheck/globbing.lua", ["luacheck.utils"] = "src/luacheck/utils.lua", ["luacheck.argparse"] = "src/luacheck/argparse.lua" }, install = { bin = { luacheck = "bin/luacheck.lua" } }, copy_directories = {"spec"} } luacheck-0.13.0/.travis.yml0000644000175000017500000000240612642521554014525 0ustar vsevavsevalanguage: c sudo: false cache: directories: cache matrix: include: - compiler: ": Lua51" env: LUA="lua 5.1" - compiler: ": Lua52" env: LUA="lua 5.2" - compiler: ": Lua53" env: LUA="lua 5.3" - compiler: ": LuaJIT20" env: LUA="luajit 2.0" - compiler: ": LuaJIT21" env: LUA="luajit 2.1" before_install: - wget https://raw.githubusercontent.com/mpeterv/hererocks/0.3.1/hererocks.py - python hererocks.py here --$LUA -r^ --downloads cache --builds cache --no-git-cache - export PATH=$PATH:$PWD/here/bin - luarocks install lanes - luarocks install busted - luarocks install luacov install: luarocks make script: - busted -c - luacheck luacheck-scm-1.rockspec -j2 - luarocks remove luafilesystem --force - luarocks remove lanes --force - lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua spec | grep 'I/O error' - lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua --version | grep 'Not found' - luarocks remove luacheck --force - lua install.lua path/to/luacheck - mv src src2 - path/to/luacheck/bin/luacheck spec/*.lua - mv src2 src after_success: - luarocks install luacov-coveralls - luacov-coveralls -v luacheck-0.13.0/.luacheckrc0000644000175000017500000000012612642521554014516 0ustar vsevavsevastd = "min" cache = true exclude_files = {"spec/*/*"} files["spec/"].std = "+busted" luacheck-0.13.0/spec/0000755000175000017500000000000012642521554013344 5ustar vsevavsevaluacheck-0.13.0/spec/expand_rockspec_spec.lua0000644000175000017500000000166512642521554020241 0ustar vsevavsevalocal expand_rockspec = require "luacheck.expand_rockspec" describe("expand_rockspec", function() it("returns sorted array of lua files related to a rock", function() assert.same({ "bar.lua", "baz.lua", "bin.lua", "foo.lua" }, expand_rockspec("spec/folder/rockspec")) end) it("returns nil, \"I/O\" for non-existent paths", function() local ok, err = expand_rockspec("spec/folder/non-existent") assert.is_nil(ok) assert.equal("I/O", err) end) it("returns nil, \"syntax\" for rockspecs with syntax errors", function() local ok, err = expand_rockspec("spec/folder/bad_config") assert.is_nil(ok) assert.equal("syntax", err) end) it("returns nil, \"runtime\" for rockspecs with run-time errors", function() local ok, err = expand_rockspec("spec/folder/bad_rockspec") assert.is_nil(ok) assert.equal("runtime", err) end) end) luacheck-0.13.0/spec/linearize_spec.lua0000644000175000017500000002232212642521554017044 0ustar vsevavsevalocal linearize = require "luacheck.linearize" local parser = require "luacheck.parser" local utils = require "luacheck.utils" local ChState = utils.class() function ChState.__init() end function ChState.warn_redefined() end function ChState.warn_global() end function ChState.warn_unused_label() end function ChState.warn_unbalanced() end function ChState.warn_empty_block() end local function get_line_(src) local ast = parser(src) local chstate = ChState() return linearize(chstate, ast) end local function get_line(src) local ok, res = pcall(get_line_, src) if ok or type(res) == "table" then return res else error(res, 0) end end local function item_to_string(item) if item.tag == "Jump" or item.tag == "Cjump" then return item.tag .. " -> " .. tostring(item.to) elseif item.tag == "Eval" then return "Eval " .. item.expr.tag elseif item.tag == "Local" then local buf = {} for _, node in ipairs(item.lhs) do table.insert(buf, ("%s (%d..%d)"):format(node.var.name, node.var.scope_start, node.var.scope_end)) end return "Local " .. table.concat(buf, ", ") else return item.tag end end local function get_line_as_string(src) local line = get_line(src) local buf = {} for i, item in ipairs(line.items) do buf[i] = tostring(i) .. ": " .. item_to_string(item) end return table.concat(buf, "\n") end local function value_info_to_string(item) local buf = {} for var, value in pairs(item.set_variables) do table.insert(buf, ("%s (%s / %s%s%s%s)"):format( var.name, var.type, value.type, value.empty and ", empty" or (value.initial and ", initial" or ""), value.secondaries and (", " .. tostring(#value.secondaries) .. " secondaries") or "", value.secondaries and value.secondaries.used and ", used" or "")) end table.sort(buf) return item.tag .. ": " .. table.concat(buf, ", ") end local function get_value_info_as_string(src) local line = get_line(src) local buf = {} for _, item in ipairs(line.items) do if item.tag == "Local" or item.tag == "Set" then assert.is_table(item.set_variables) table.insert(buf, value_info_to_string(item)) end end return table.concat(buf, "\n") end describe("linearize", function() describe("when handling post-parse syntax errors", function() it("detects gotos without labels", function() assert.same({line = 1, column = 1, end_column = 4, msg = "no visible label 'fail'"}, get_line("goto fail")) end) it("detects break outside loops", function() assert.same({line = 1, column = 1, end_column = 5, msg = "'break' is not inside a loop"}, get_line("break")) assert.same({line = 1, column = 28, end_column = 32, msg = "'break' is not inside a loop"}, get_line("while true do function f() break end end")) end) it("detects duplicate labels", function() assert.same({line = 2, column = 1, end_column = 8, msg = "label 'fail' already defined on line 1"}, get_line("::fail::\n::fail::")) end) it("detects varargs outside vararg functions", function() assert.same({line = 1, column = 21, end_column = 23, msg = "cannot use '...' outside a vararg function"}, get_line("function f() return ... end")) assert.same({line = 1, column = 42, end_column = 44, msg = "cannot use '...' outside a vararg function"}, get_line("function f(...) return function() return ... end end")) end) end) describe("when linearizing flow", function() it("linearizes empty source correctly", function() assert.equal("1: Local ... (2..1)", get_line_as_string("")) end) it("linearizes do-end blocks correctly", function() assert.equal([[ 1: Local ... (2..4) 2: Noop 3: Noop 4: Eval Call]], get_line_as_string([[ do end do print(foo) end]])) end) it("linearizes loops correctly", function() assert.equal([[ 1: Local ... (2..8) 2: Noop 3: Eval Id 4: Cjump -> 9 5: Local s (6..6) 6: Eval Call 7: Noop 8: Jump -> 3]], get_line_as_string([[ while cond do local s = io.read() print(s) end]])) assert.equal([[ 1: Local ... (2..6) 2: Noop 3: Local s (4..5) 4: Eval Call 5: Eval Id 6: Cjump -> 3]], get_line_as_string([[ repeat local s = io.read() print(s) until cond]])) assert.equal([[ 1: Local ... (2..9) 2: Noop 3: Eval Number 4: Eval Op 5: Cjump -> 10 6: Local i (7..7) 7: Eval Call 8: Noop 9: Jump -> 5]], get_line_as_string([[ for i = 1, #t do print(t[i]) end]])) assert.equal([[ 1: Local ... (2..8) 2: Noop 3: Eval Call 4: Cjump -> 9 5: Local k (6..6), v (6..6) 6: Eval Call 7: Noop 8: Jump -> 4]], get_line_as_string([[ for k, v in pairs(t) do print(k, v) end]])) end) it("linearizes loops with literal condition correctly", function() assert.equal([[ 1: Local ... (2..6) 2: Noop 3: Eval Number 4: Eval Call 5: Noop 6: Jump -> 3]], get_line_as_string([[ while 1 do foo() end]])) assert.equal([[ 1: Local ... (2..7) 2: Noop 3: Eval False 4: Jump -> 8 5: Eval Call 6: Noop 7: Jump -> 3]], get_line_as_string([[ while false do foo() end]])) assert.equal([[ 1: Local ... (2..4) 2: Noop 3: Eval Call 4: Eval True]], get_line_as_string([[ repeat foo() until true]])) assert.equal([[ 1: Local ... (2..5) 2: Noop 3: Eval Call 4: Eval Nil 5: Jump -> 3]], get_line_as_string([[ repeat foo() until nil]])) end) it("linearizes nested loops and breaks correctly", function() assert.equal([[ 1: Local ... (2..24) 2: Noop 3: Eval Call 4: Cjump -> 25 5: Eval Call 6: Noop 7: Eval Call 8: Cjump -> 18 9: Eval Call 10: Noop 11: Eval Call 12: Cjump -> 15 13: Jump -> 18 14: Jump -> 15 15: Eval Call 16: Noop 17: Jump -> 7 18: Noop 19: Eval Call 20: Cjump -> 23 21: Jump -> 25 22: Jump -> 23 23: Noop 24: Jump -> 3]], get_line_as_string([[ while cond() do stmts() while cond() do stmts() if cond() then break end stmts() end if cond() then break end end]])) end) it("linearizes if correctly", function() assert.equal([[ 1: Local ... (2..15) 2: Noop 3: Eval Call 4: Cjump -> 16 5: Noop 6: Eval Call 7: Cjump -> 10 8: Eval Call 9: Jump -> 15 10: Eval Call 11: Cjump -> 14 12: Eval Call 13: Jump -> 15 14: Eval Call 15: Jump -> 16]], get_line_as_string([[ if cond() then if cond() then stmts() elseif cond() then stmts() else stmts() end end]])) end) it("linearizes if with literal condition correctly", function() assert.equal([[ 1: Local ... (2..14) 2: Noop 3: Eval True 4: Noop 5: Eval Call 6: Cjump -> 9 7: Eval Call 8: Jump -> 14 9: Eval False 10: Jump -> 13 11: Eval Call 12: Jump -> 14 13: Eval Call 14: Jump -> 15]], get_line_as_string([[ if true then if cond() then stmts() elseif false then stmts() else stmts() end end]])) end) it("linearizes gotos correctly", function() assert.equal([[ 1: Local ... (2..13) 2: Eval Call 3: Noop 4: Jump -> 2 5: Eval Call 6: Noop 7: Jump -> 9 8: Eval Call 9: Eval Call 10: Noop 11: Noop 12: Jump -> 14 13: Eval Call]], get_line_as_string([[ ::label1:: stmts() goto label1 stmts() goto label2 stmts() ::label2:: stmts() do goto label2 stmts() ::label2:: end]])) end) end) describe("when registering values", function() it("registers values in empty chunk correctly", function() assert.equal([[ Local: ... (arg / arg, initial)]], get_value_info_as_string("")) end) it("registers values in assignments correctly", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial) Set: a (var / var)]], get_value_info_as_string([[ local a = b a = d]])) end) it("registers empty values correctly", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial), b (var / var, empty) Set: a (var / var), b (var / var)]], get_value_info_as_string([[ local a, b = 4 a, b = 5]])) end) it("registers function values as of type func", function() assert.equal([[ Local: ... (arg / arg, initial) Local: f (var / func, initial)]], get_value_info_as_string([[ local function f() end]])) end) it("registers overwritten args and counters as of type var", function() assert.equal([[ Local: ... (arg / arg, initial) Local: i (loopi / loopi, initial) Set: i (loopi / var)]], get_value_info_as_string([[ for i = 1, 10 do i = 6 end]])) end) it("registers groups of secondary values", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial), b (var / var, initial, 2 secondaries), c (var / var, initial, 2 secondaries) Set: a (var / var), b (var / var, 2 secondaries), c (var / var, 2 secondaries)]], get_value_info_as_string([[ local a, b, c = f(), g() a, b, c = f(), g()]])) end) it("marks groups of secondary values used if one of values is put into global or index", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, empty) Set: a (var / var, 1 secondaries, used)]], get_value_info_as_string([[ local a g, a = f()]])) end) end) end) luacheck-0.13.0/spec/luacheck_spec.lua0000644000175000017500000003221412642521554016642 0ustar vsevavsevalocal luacheck = require "luacheck" local function strip_locations(report) for _, file_report in ipairs(report) do for _, event in ipairs(file_report) do event.line = nil event.column = nil event.end_column = nil event.prev_line = nil event.prev_column = nil end end return report end describe("luacheck", function() it("is an alias of luacheck.check_files", function() assert.same(luacheck.check_files({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }), luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" })) end) it("panics on bad files", function() assert.has_error(function() luacheck("foo") end, "bad argument #1 to 'luacheck.check_files' (table expected, got string)") assert.has_error(function() luacheck({123}) end, "bad argument #1 to 'luacheck.check_files' (array of paths or file handles expected, got number)") end) it("panics on bad options", function() assert.has_error(function() luacheck({"foo"}, "bar") end, "bad argument #2 to 'luacheck.check_files' (table or nil expected, got string)") assert.has_error(function() luacheck({"foo"}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.check_files' (invalid value of option 'globals')") assert.has_error(function() luacheck({"foo"}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.check_files' (invalid value of option 'unused')") end) it("works on empty list", function() assert.same({ warnings = 0, errors = 0, fatals = 0 }, strip_locations(luacheck({}))) end) it("works on files", function() assert.same({ {}, { { code = "211", name = "helper", func = true }, { code = "212", name = "..." }, { code = "111", name = "embrace", top = true }, { code = "412", name = "opt" }, { code = "113", name = "hepler" } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 5, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }))) end) it("uses options", function() assert.same({ {}, { { code = "111", name = "embrace", top = true }, { code = "412", name = "opt" }, { code = "113", name = "hepler" } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 3, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }, { unused = false }))) end) it("uses option overrides", function() assert.same({ {}, { { code = "111", name = "embrace", top = true }, { code = "113", name = "hepler" } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 2, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }, { nil, { global = true, unused = false, redefined = false }, global = false }))) end) end) describe("check_strings", function() it("panics on bad strings", function() assert.has_error(function() luacheck.check_strings("foo") end, "bad argument #1 to 'luacheck.check_strings' (table expected, got string)") assert.has_error(function() luacheck.check_strings({1}) end, "bad argument #1 to 'luacheck.check_strings' (array of strings or tables expected, got number)") end) it("panics on bad options", function() assert.has_error(function() luacheck.check_strings({"foo"}, "bar") end, "bad argument #2 to 'luacheck.check_strings' (table or nil expected, got string)") assert.has_error(function() luacheck.check_strings({"foo"}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.check_strings' (invalid value of option 'globals')") assert.has_error(function() luacheck.check_strings({"foo"}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.check_strings' (invalid value of option 'unused')") end) it("works on empty list", function() assert.same({ warnings = 0, errors = 0, fatals = 0 }, luacheck.check_strings({})) end) it("works on strings", function() assert.same({ { { code = "113", name = "foo" } }, { { code = "011", msg = "expected expression near 'return'" } }, warnings = 1, errors = 1, fatals = 0 }, strip_locations(luacheck.check_strings({"return foo", "return return"}))) end) it("provides correct location info for warnings", function() assert.same({ { { code = "521", name = "foo", line = 1, column = 1, end_column = 6 }, { code = "312", name = "self", line = 3, column = 11, end_column = 11 }, { code = "311", name = "self", line = 4, column = 4, end_column = 7 }, { code = "511", line = 9, column = 1, end_column = 1 } }, warnings = 4, errors = 0, fatals = 0 }, luacheck.check_strings({[[ :: foo ::local t = {} function t:m(x) self = x self = x return self end do return t end (t)() ]]})) end) it("provides correct location info for bad inline options", function() assert.same({ { { code = "022", line = 1, column = 1, end_column = 17 }, { code = "023", line = 3, column = 4, end_column = 26 }, { code = "021", line = 6, column = 10, end_column = 14 } }, warnings = 0, errors = 3, fatals = 0 }, luacheck.check_strings({[[ -- luacheck: push local function f() --[=[ luacheck: pop ]=] end return f --[=[ luacheck: some invalid comment ]=] ]]})) end) it("provides correct location info for syntax errors", function() assert.same({ { { code = "011", msg = "unfinished string", line = 1, column = 11, end_column = 11 } }, { { code = "011", msg = "invalid hexadecimal escape sequence '\\x2'", line = 1, column = 15, end_column = 17 } }, { { code = "011", msg = "expected 'then' near ", line = 1, column = 9, end_column = 9 } }, { { code = "011", msg = "label 'b' already defined on line 1", line = 1, column = 7, end_column = 11 } }, { { code = "011", msg = "cannot use '...' outside a vararg function", line = 1, column = 15, end_column = 17 } }, { { code = "011", msg = "'break' is not inside a loop", line = 1, column = 1, end_column = 5 } }, warnings = 0, errors = 6, fatals = 0 }, luacheck.check_strings({ [[local x = "foo]], [[local x = "foo\x2]], [[if true ]], [[::b:: ::b::]], [[function f() (...)() end]], [[break it()]] })) end) it("uses options", function() assert.same({ {}, { { code = "011", msg = "expected expression near 'return'" } }, warnings = 0, errors = 1, fatals = 0 }, strip_locations(luacheck.check_strings({"return foo", "return return"}, {ignore = {"113"}}))) end) it("ignores tables with .fatal field", function() assert.same({ { { code = "113", name = "foo" } }, { fatal = "I/O" }, warnings = 1, errors = 0, fatals = 1 }, strip_locations(luacheck.check_strings({"return foo", {fatal = "I/O"}}))) end) end) describe("get_report", function() it("panics on bad argument", function() assert.has_error(function() luacheck.get_report({}) end, "bad argument #1 to 'luacheck.get_report' (string expected, got table)") end) it("returns a table", function() assert.is_table(luacheck.get_report("return foo")) end) it("returns a table with single error event on syntax error", function() local report = strip_locations({luacheck.get_report("return return")})[1] assert.same({code = "011", msg = "expected expression near 'return'"}, report[1]) end) end) describe("process_reports", function() it("panics on bad reports", function() assert.has_error(function() luacheck.process_reports("foo") end, "bad argument #1 to 'luacheck.process_reports' (table expected, got string)") end) it("panics on bad options", function() assert.has_error(function() luacheck.process_reports({{}}, "bar") end, "bad argument #2 to 'luacheck.process_reports' (table or nil expected, got string)") assert.has_error(function() luacheck.process_reports({{}}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.process_reports' (invalid value of option 'globals')") assert.has_error(function() luacheck.process_reports({{}}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.process_reports' (invalid value of option 'unused')") end) it("processes reports", function() assert.same({ { { code = "113", name = "foo" } }, {}, warnings = 1, errors = 0, fatals = 0 }, strip_locations(luacheck.process_reports({luacheck.get_report("return foo"), luacheck.get_report("return math")}))) end) it("uses options", function() assert.same({ { { code = "113", name = "foo" } }, { { code = "113", name = "math" } }, warnings = 2, errors = 0, fatals = 0 }, strip_locations(luacheck.process_reports({luacheck.get_report("return foo"), luacheck.get_report("return math")}, { std = "none" }))) end) end) describe("get_message", function() it("panics on bad events", function() assert.has_error(function() luacheck.get_message("foo") end, "bad argument #1 to 'luacheck.get_message' (table expected, got string)") end) it("returns message for an event", function() assert.equal("unused argument 'bar'", luacheck.get_message({ code = "212", name = "bar" })) assert.equal("shadowing definition of loop variable 'foo' on line 1", luacheck.get_message({ code = "423", name = "foo", line = 2, prev_line = 1 })) assert.equal("message goes here", luacheck.get_message({ code = "011", msg = "message goes here" })) assert.equal("unexpected character near '%'", luacheck.get_message({ code = "011", msg = "unexpected character near '%'" })) end) end) luacheck-0.13.0/spec/cache_spec.lua0000644000175000017500000002035612642521554016132 0ustar vsevavsevalocal cache = require "luacheck.cache" local utils = require "luacheck.utils" local actual_format_version setup(function() actual_format_version = cache.format_version cache.format_version = 0 end) teardown(function() cache.format_version = actual_format_version end) describe("cache", function() describe("serialize", function() it("returns serialized result", function() assert.same( [[return {{"111","foo",5,100,102,[22]=true},{"211","bar",4,1,3,[8]=true,[11]=true},{"011",[4]=100000,[23]="near '\"'"}}]], cache.serialize({ {code = "111", name = "foo", line = 5, column = 100, end_column = 102, in_module = true}, {code = "211", name = "bar", line = 4, column = 1, end_column = 3, secondary = true, filtered = true}, {code = "011", column = 100000, msg = "near '\"'"} }) ) end) it("puts repeating string values into locals", function() assert.same( [[local A,B="111","foo";return {{A,B,5,100,[22]=true},{A,B,6,100,[8]=true,[11]=true},{"011",[4]=100000,[23]="near '\"'"}}]], cache.serialize({ {code = "111", name = "foo", line = 5, column = 100, in_module = true}, {code = "111", name = "foo", line = 6, column = 100, secondary = true, filtered = true}, {code = "011", column = 100000, msg = "near '\"'"} }) ) end) it("uses at most 52 locals", function() assert.same( 'local A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z=' .. '"111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127","128",' .. '"129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146",' .. '"147","148","149","150","151","152","153","154","155","156","157","158","159","160","161","162";' .. 'return {{A,A},{B,B},{C,C},{D,D},{E,E},{F,F},{G,G},{H,H},{I,I},{J,J},{K,K},{L,L},{M,M},{N,N},{O,O},' .. '{P,P},{Q,Q},{R,R},{S,S},{T,T},{U,U},{V,V},{W,W},{X,X},{Y,Y},{Z,Z},' .. '{a,a},{b,b},{c,c},{d,d},{e,e},{f,f},{g,g},{h,h},{i,i},{j,j},{k,k},{l,l},{m,m},{n,n},{o,o},' .. '{p,p},{q,q},{r,r},{s,s},{t,t},{u,u},{v,v},{w,w},{x,x},{y,y},{z,z},{"163","163"},{"164","164"}}', cache.serialize({ {code = "111", name = "111"}, {code = "112", name = "112"}, {code = "113", name = "113"}, {code = "114", name = "114"}, {code = "115", name = "115"}, {code = "116", name = "116"}, {code = "117", name = "117"}, {code = "118", name = "118"}, {code = "119", name = "119"}, {code = "120", name = "120"}, {code = "121", name = "121"}, {code = "122", name = "122"}, {code = "123", name = "123"}, {code = "124", name = "124"}, {code = "125", name = "125"}, {code = "126", name = "126"}, {code = "127", name = "127"}, {code = "128", name = "128"}, {code = "129", name = "129"}, {code = "130", name = "130"}, {code = "131", name = "131"}, {code = "132", name = "132"}, {code = "133", name = "133"}, {code = "134", name = "134"}, {code = "135", name = "135"}, {code = "136", name = "136"}, {code = "137", name = "137"}, {code = "138", name = "138"}, {code = "139", name = "139"}, {code = "140", name = "140"}, {code = "141", name = "141"}, {code = "142", name = "142"}, {code = "143", name = "143"}, {code = "144", name = "144"}, {code = "145", name = "145"}, {code = "146", name = "146"}, {code = "147", name = "147"}, {code = "148", name = "148"}, {code = "149", name = "149"}, {code = "150", name = "150"}, {code = "151", name = "151"}, {code = "152", name = "152"}, {code = "153", name = "153"}, {code = "154", name = "154"}, {code = "155", name = "155"}, {code = "156", name = "156"}, {code = "157", name = "157"}, {code = "158", name = "158"}, {code = "159", name = "159"}, {code = "160", name = "160"}, {code = "161", name = "161"}, {code = "162", name = "162"}, {code = "163", name = "163"}, {code = "164", name = "164"} }) ) end) it("handles error result", function() assert.same('return {{"011",[3]=2,[4]=4,[23]="message"}}', cache.serialize({{code = "011", line = 2, column = 4, msg = "message"}})) end) end) describe("update", function() local tmpname before_each(function() tmpname = os.tmpname() end) after_each(function() os.remove(tmpname) end) it("creates new cache", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {{{code="111"}}, {}, {{code="112"}}}) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 1 return {{"112"}} bar 2 return {} ]], data) end) it("appends new entries", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {{{code="111"}}, {}, {{code="112"}}}) local ok, appended = cache.update(tmpname, {"baz"}, {3}, {{{code="111"},{code="122"}}}) assert.is_true(ok) assert.is_true(appended) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 1 return {{"112"}} bar 2 return {} baz 3 return {{"111"},{"122"}} ]], data) end) it("overwrites old entries", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {{{code="111"}}, {}, {{code="112"}}}) local ok, appended = cache.update(tmpname, {"baz", "foo"}, {3, 4}, {{{code="111"},{code="122"}}, {}}) assert.is_true(ok) assert.is_false(appended) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 4 return {} bar 2 return {} baz 3 return {{"111"},{"122"}} ]], data) end) end) describe("load", function() describe("error handling", function() it("returns {} on cache with bad version", function() assert.same({}, cache.load("spec/caches/different_format.cache", {"foo"}, {123})) end) it("returns {} on cache without version", function() assert.same({}, cache.load("spec/caches/old_format.cache", {"foo"}, {123})) end) it("returns nil on cache with bad number of lines", function() assert.is_nil(cache.load("spec/caches/bad_lines.cache", {"foo"}, {123})) end) it("returns nil on cache with bad mtime", function() assert.is_nil(cache.load("spec/caches/bad_mtime.cache", {"foo"}, {123})) end) it("returns nil on cache with bad result", function() assert.is_nil(cache.load("spec/caches/bad_result.cache", {"foo"}, {123})) assert.is_nil(cache.load("spec/caches/bad_result2.cache", {"foo"}, {123})) end) end) describe("loading", function() local tmpname before_each(function() tmpname = os.tmpname() cache.update(tmpname, {"foo", "bar"}, {1, 2}, {{{code="111"}}, {{code = "011", line = 2, column = 4, msg = "message"}}}) end) after_each(function() os.remove(tmpname) end) it("loads {} from non-existent cache", function() assert.same({}, cache.load("non-existent.file", {"foo"})) end) it("loads cached results", function() assert.same({ foo = {{code="111"}}, bar = {{code = "011", line = 2, column = 4, msg = "message"}} }, cache.load(tmpname, {"foo", "bar"}, {1, 2})) end) it("does not load results for missing files", function() assert.same({foo = {{code="111"}}}, cache.load(tmpname, {"foo", "baz"}, {1, 2})) end) it("does not load outdated results", function() assert.same( {bar = {{code = "011", line = 2, column = 4, msg = "message"}}}, cache.load(tmpname, {"foo", "bar", "baz"}, {2, 2})) end) end) end) end) luacheck-0.13.0/spec/filter_spec.lua0000644000175000017500000002336512642521554016357 0ustar vsevavsevalocal filter = require "luacheck.filter".filter describe("filter", function() it("filters warnings by name", function() assert.same({ { { code = "211", name = "baz" } } }, filter({ { { code = "211", name = "foo" }, { code = "211", name = "bar" }, { code = "211", name = "baz" } } }, { ignore = {"bar"}, only = {"bar", "baz"} })) end) it("removes unused var/value and redefined warnings related to _", function() assert.same({ { { code = "211", name = "foo" } } }, filter({ { { code = "211", name = "foo" }, { code = "211", name = "_" }, { code = "412", name = "_" }, { code = "221", name = "_" } } })) end) it("filters warnings by type", function() assert.same({ { { code = "211", name = "foo" } } }, filter({ { { code = "211", name = "foo" }, { code = "111", name = "bar" }, { code = "413", name = "baz" } } }, { global = false, redefined = false })) assert.same({ { { code = "221", name = "foo" }, { code = "111", name = "bar" }, { code = "413", name = "baz" } } }, filter({ { { code = "221", name = "foo" }, { code = "111", name = "bar" }, { code = "321", name = "qu" }, { code = "413", name = "baz" } } }, { ignore = {"32"} })) end) it("filters warnings by code and name using patterns", function() assert.same({ { { code = "212", name = "bar" }, { code = "413", name = "_baz" } } }, filter({ { { code = "212", name = "bar" }, { code = "212", name = "_qu" }, { code = "321", name = "foo" }, { code = "413", name = "_baz" } } }, { ignore = {"foo", "212/_.*"} })) end) it("filters unused warnings by subtype", function() assert.same({ { { code = "211", name = "foo" } } }, filter({ { { code = "211", name = "foo" }, { code = "311", name = "bar" }, { code = "212", name = "baz" }, { code = "221", name = "qu" } } }, { ignore = {"22", "31"}, unused_args = false })) end) it("filters unused warnings related to secondary variables", function() assert.same({ { { code = "212", name = "baz" } } }, filter({ { { code = "211", name = "foo", secondary = true }, { code = "311", name = "bar", secondary = true }, { code = "212", name = "baz" } } }, { unused_secondaries = false })) end) it("filters unused and redefined warnings related to implicit self", function() assert.same({ { { code = "212", name = "self" } } }, filter({ { { code = "212", name = "self", self = true }, { code = "432", name = "self", self = true }, { code = "212", name = "self" } } }, { self = false })) end) it("filters defined globals", function() assert.same({ { { code = "111", name = "module" } } }, filter({ { { code = "113", name = "foo" }, { code = "111", name = "module" } } }, { std = {}, globals = {"foo"} })) end) it("filters standard globals", function() assert.same({ { { code = "111", name = "module" } } }, filter({ { { code = "113", name = "package" }, { code = "111", name = "module" } } }, { std = "min" })) end) it("allows defined globals with allow_defined = true", function() assert.same({ { { code = "131", name = "bar" }, { code = "113", name = "baz" } } }, filter({ { { code = "113", name = "foo" }, { code = "111", name = "foo" }, { code = "111", name = "bar" }, { code = "113", name = "baz" } } }, { allow_defined = true })) end) it("allows globals defined in top level function scope with allow_defined_top = true", function() assert.same({ { { code = "111", name = "bar" }, { code = "113", name = "baz" } } }, filter({ { { code = "113", name = "foo" }, { code = "111", name = "foo", top = true }, { code = "111", name = "bar" }, { code = "113", name = "baz" } } }, { allow_defined_top = true })) end) it("allows globals defined in the same file with module = true", function() assert.same({ {}, { { code = "113", name = "foo" } } }, filter({ { { code = "113", name = "foo" }, { code = "111", name = "foo" } }, { { code = "113", name = "foo" } } }, { allow_defined = true, module = true })) end) it("only allows setting globals defined in the same file with module = true", function() assert.same({ {}, { { code = "111", name = "string", module = true }, { code = "111", name = "bar", module = true } } }, filter({ { { code = "111", name = "bar" } }, { { code = "111", name = "foo", top = true }, { code = "111", name = "foo", }, { code = "111", name = "string" }, { code = "111", name = "bar" } } }, { { allow_defined = true, ignore = {"13"} }, { allow_defined_top = true, module = true } })) end) it("using an implicitly defined global from a module marks it as used", function() assert.same({ {}, {} }, filter({ { { code = "111", name = "foo" } }, { { code = "113", name = "foo" }, { code = "111", name = "bar", } } }, { { allow_defined = true }, { allow_defined = true, module = true } })) end) end) luacheck-0.13.0/spec/utils_spec.lua0000644000175000017500000001416712642521554016232 0ustar vsevavsevalocal utils = require "luacheck.utils" describe("utils", function() describe("read_file", function() it("returns contents of a file", function() assert.equal("contents\n", utils.read_file("spec/folder/foo")) end) it("removes UTF-8 BOM", function() assert.equal("foo\nbar\n", utils.read_file("spec/folder/bom")) end) it("returns nil for non-existent paths", function() assert.is_nil(utils.read_file("spec/folder/non-existent")) end) it("returns nil for directories", function() assert.is_nil(utils.read_file("spec/folder")) end) end) describe("load", function() it("loads function in an environment", function() local f = utils.load("return g", {g = "foo"}) assert.is_function(f) assert.is_equal("foo", f()) end) it("returns nil on syntax error", function() assert.is_nil(utils.load("return return", {})) end) end) describe("load_config", function() it("loads config from a file and returns it", function() assert.same({foo = "bar"}, (utils.load_config("spec/folder/config"))) end) it("passes second argument as environment", function() local function bar() return "bar" end assert.same({ foo = "bar", bar = bar }, (utils.load_config("spec/folder/env_config", {bar = bar}))) end) it("returns nil, \"I/O\" for non-existent paths", function() local ok, err = utils.load_config("spec/folder/non-existent") assert.is_nil(ok) assert.equal("I/O", err) end) it("returns nil, \"syntax\" for configs with syntax errors", function() local ok, err = utils.load_config("spec/folder/bad_config") assert.is_nil(ok) assert.equal("syntax", err) end) it("returns nil, \"runtime\" for configs with run-time errors", function() local ok, err = utils.load_config("spec/folder/env_config") assert.is_nil(ok) assert.equal("runtime", err) end) end) describe("array_to_set", function() it("converts array to set and returns it", function() assert.same({foo = 3, bar = 2}, utils.array_to_set({"foo", "bar", "foo"})) end) end) describe("concat_arrays", function() it("returns concatenated arrays", function() assert.same({1, 2, 3, 4}, utils.concat_arrays({{}, {1}, {2, 3, 4}, {}})) end) end) describe("update", function() it("updates first table with entries from second", function() local t1 = {k1 = 1, k2 = 2} local t2 = {k2 = 3, k3 = 4} local ret = utils.update(t1, t2) assert.same({k1 = 1, k2 = 3, k3 = 4}, t1) assert.equal(t1, ret) end) end) describe("class", function() it("returns an object creator", function() local cl = utils.class() assert.is_table(cl) cl.field = "foo" local obj = cl() assert.is_table(obj) obj.field2 = "bar" assert.equal("foo", obj.field) assert.is_nil(cl.field2) end) it("calls __init on object creation", function() local cl = utils.class() cl.__init = spy.new(function() end) local obj = cl("foo", "bar") assert.spy(cl.__init).was_called(1) assert.spy(cl.__init).was_called_with(obj, "foo", "bar") end) end) describe("Stack", function() it("supports push/pop operations and top/size fields", function() local stack = utils.Stack() assert.equal(0, stack.size) assert.is_nil(stack.top) stack:push(7) stack:push(8) assert.equal(2, stack.size) assert.equal(8, stack.top) assert.equal(8, stack:pop()) assert.equal(1, stack.size) assert.equal(7, stack.top) stack:push(4) assert.equal(2, stack.size) assert.equal(4, stack.top) assert.equal(4, stack:pop()) assert.equal(7, stack:pop()) assert.equal(0, stack.size) assert.is_nil(stack.top) end) end) describe("pcall", function() it("calls f with arg", function() assert.equal(3, utils.pcall(math.sqrt, 9)) end) it("returns nil, table if f throws a table", function() local t = {"foo"} local res, err = utils.pcall(function(x) if x == 9 then error(t) else return true end end, 9) assert.is_nil(res) assert.is_equal(t, err) end) it("rethrows if f crashes (throws not a table)", function() local ok, err = pcall(utils.pcall, error, "msg") assert.is_false(ok) assert.matches("msg\nstack traceback:", err) end) end) describe("ripairs", function() it("returns reversed ipairs", function() local arr = {foo = "bar", 5, 6, 7} local iterated = {} for i, v in utils.ripairs(arr) do table.insert(iterated, {i, v}) end assert.same({{3, 7}, {2, 6}, {1, 5}}, iterated) end) end) describe("after", function() it("returns substring after match", function() assert.equal("foo bar: baz", utils.after("bar: foo bar: baz", "bar:%s*")) end) it("returns nil when there is no match", function() assert.is_nil(utils.after("bar: foo bar: baz", "baz:%s*")) end) end) describe("strip", function() it("returns string without whitespace on ends", function() assert.equal("foo bar", utils.strip("\tfoo bar\n ")) end) end) describe("split", function() it("without separator, returns non-whitespace substrings", function() assert.same({"foo", "bar", "baz"}, utils.split(" foo bar\n baz ")) end) it("with separator, returns substrings between them", function() assert.same({"", "foo", " bar", "", " baz "}, utils.split(",foo, bar,, baz ", ",")) end) end) describe("map", function() it("maps function over an array", function() assert.same({3, 1, 2}, utils.map(math.sqrt, {9, 1, 4})) end) end) end) luacheck-0.13.0/spec/parser_spec.lua0000644000175000017500000014030712642521554016362 0ustar vsevavsevalocal parser = require "luacheck.parser" local function strip_locations(ast) ast.location = nil ast.end_location = nil ast.end_column = nil ast.equals_location = nil ast.first_token = nil for i=1, #ast do if type(ast[i]) == "table" then strip_locations(ast[i]) end end end local function get_ast(src) local ast = parser(src) assert.is_table(ast) strip_locations(ast) return ast end local function get_node(src) return get_ast(src)[1] end local function get_expr(src) return get_node("return " .. src)[1] end local function get_comments(src) return (select(2, parser(src))) end local function get_code_lines(src) return select(3, parser(src)) end local function get_error(src) local ok, err = pcall(parser, src) assert.is_false(ok) return err end describe("parser", function() it("parses empty source correctly", function() assert.same({}, get_ast(" ")) end) it("does not allow extra ending keywords", function() assert.same({line = 1, column = 1, end_column = 3, msg = "expected near 'end'"}, get_error("end")) end) it("parses return statement correctly", function() assert.same({tag = "Return"}, get_node("return")) assert.same({tag = "Return", {tag = "Number", "1"} }, get_node("return 1")) assert.same({tag = "Return", {tag = "Number", "1"}, {tag = "String", "foo"} }, get_node("return 1, 'foo'")) assert.same({line = 1, column = 10, end_column = 10, msg = "expected expression near "}, get_error("return 1,")) end) it("parses labels correctly", function() assert.same({tag = "Label", "fail"}, get_node("::fail::")) assert.same({tag = "Label", "fail"}, get_node("::\nfail\n::")) assert.same({line = 1, column = 3, end_column = 4, msg = "expected identifier near '::'"}, get_error("::::")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected identifier near '1'"}, get_error("::1::")) end) it("parses goto correctly", function() assert.same({tag = "Goto", "fail"}, get_node("goto fail")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected identifier near "}, get_error("goto")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected statement near ','"}, get_error("goto foo, bar")) end) it("parses break correctly", function() assert.same({tag = "Break"}, get_node("break")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected '=' near "}, get_error("break fail")) end) it("parses do end correctly", function() assert.same({tag = "Do"}, get_node("do end")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected 'end' near "}, get_error("do")) assert.same({line = 1, column = 4, end_column = 8, msg = "expected 'end' near 'until'"}, get_error("do until false")) assert.same({line = 2, column = 1, end_column = 5, msg = "expected 'end' (to close 'do' on line 1) near 'until'"}, get_error("do\nuntil false")) end) it("parses while do end correctly", function() assert.same({tag = "While", {tag = "True"}, {} }, get_node("while true do end")) assert.same({line = 1, column = 6, end_column = 6, msg = "expected condition near "}, get_error("while")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected 'do' near "}, get_error("while true")) assert.same({line = 1, column = 14, end_column = 14, msg = "expected 'end' near "}, get_error("while true do")) assert.same({line = 2, column = 3, end_column = 3, msg = "expected 'end' (to close 'while' on line 1) near "}, get_error("while true\ndo")) assert.same({line = 1, column = 7, end_column = 8, msg = "expected condition near 'do'"}, get_error("while do end")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected 'do' near ','"}, get_error("while true, false do end")) end) it("parses repeat until correctly", function() assert.same({tag = "Repeat", {}, {tag = "True"} }, get_node("repeat until true")) assert.same({line = 1, column = 7, end_column = 7, msg = "expected 'until' near "}, get_error("repeat")) assert.same({line = 3, column = 1, end_column = 1, msg = "expected 'until' (to close 'repeat' on line 1) near "}, get_error("repeat\n--")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected condition near "}, get_error("repeat until")) assert.same({line = 1, column = 18, end_column = 18, msg = "expected statement near ','"}, get_error("repeat until true, false")) end) describe("when parsing if", function() it("parses if then end correctly", function() assert.same({tag = "If", {tag = "True"}, {} }, get_node("if true then end")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected condition near "}, get_error("if")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected 'then' near "}, get_error("if true")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected 'end' near "}, get_error("if true then")) assert.same({line = 2, column = 5, end_column = 5, msg = "expected 'end' (to close 'if' on line 1) near "}, get_error("if true\nthen")) assert.same({line = 1, column = 4, end_column = 7, msg = "expected condition near 'then'"}, get_error("if then end")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected 'then' near ','"}, get_error("if true, false then end")) end) it("parses if then else end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {} }, get_node("if true then else end")) assert.same({line = 1, column = 18, end_column = 18, msg = "expected 'end' near "}, get_error("if true then else")) assert.same({line = 3, column = 1, end_column = 1, msg = "expected 'end' (to close 'else' on line 2) near "}, get_error("if true\nthen else\n")) assert.same({line = 1, column = 19, end_column = 22, msg = "expected 'end' near 'else'"}, get_error("if true then else else end")) end) it("parses if then elseif then end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {tag = "False"}, {} }, get_node("if true then elseif false then end")) assert.same({line = 1, column = 21, end_column = 23, msg = "expected condition near 'end'"}, get_error("if true then elseif end")) assert.same({line = 1, column = 21, end_column = 24, msg = "expected condition near 'then'"}, get_error("if true then elseif then end")) assert.same({line = 2, column = 5, end_column = 5, msg = "expected 'end' (to close 'elseif' on line 1) near "}, get_error("if true then elseif a\nthen")) end) it("parses if then elseif then else end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {tag = "False"}, {}, {} }, get_node("if true then elseif false then else end")) assert.same({line = 1, column = 36, end_column = 36, msg = "expected 'end' near "}, get_error("if true then elseif false then else")) end) end) describe("when parsing for", function() it("parses fornum correctly", function() assert.same({tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Op", "len", {tag = "Id", "t"}}, {} }, get_node("for i=1, #t do end")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected identifier near "}, get_error("for")) assert.same({line = 1, column = 6, end_column = 6, msg = "expected '=', ',' or 'in' near "}, get_error("for i")) assert.same({line = 1, column = 7, end_column = 8, msg = "expected '=', ',' or 'in' near '~='"}, get_error("for i ~= 2")) assert.same({line = 1, column = 11, end_column = 12, msg = "expected ',' near 'do'"}, get_error("for i = 2 do end")) assert.same({line = 1, column = 15, end_column = 15, msg = "expected 'end' near "}, get_error("for i=1, #t do")) assert.same({line = 2, column = 4, end_column = 4, msg = "expected 'end' (to close 'for' on line 1) near "}, get_error("for i=1, #t do\na()")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected identifier near '('"}, get_error("for (i)=1, #t do end")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected identifier near '3'"}, get_error("for 3=1, #t do end")) end) it("parses fornum with step correctly", function() assert.same({tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Op", "len", {tag = "Id", "t"}}, {tag = "Number", "2"}, {} }, get_node("for i=1, #t, 2 do end")) assert.same({line = 1, column = 15, end_column = 15, msg = "expected 'do' near ','"}, get_error("for i=1, #t, 2, 3 do")) end) it("parses forin correctly", function() assert.same({tag = "Forin", { {tag = "Id", "i"} }, { {tag = "Id", "t"} }, {} }, get_node("for i in t do end")) assert.same({tag = "Forin", { {tag = "Id", "i"}, {tag = "Id", "j"} }, { {tag = "Id", "t"}, {tag = "String", "foo"} }, {} }, get_node("for i, j in t, 'foo' do end")) assert.same({line = 1, column = 5, end_column = 6, msg = "expected identifier near 'in'"}, get_error("for in foo do end")) assert.same({line = 1, column = 10, end_column = 11, msg = "expected expression near 'do'"}, get_error("for i in do end")) end) end) describe("when parsing functions", function() it("parses simple function correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {}, {}} } }, get_node("function a() end")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, get_error("function")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected '(' near "}, get_error("function a")) assert.same({line = 1, column = 12, end_column = 12, msg = "expected argument near "}, get_error("function a(")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected 'end' near "}, get_error("function a()")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected 'end' (to close 'function' on line 1) near "}, get_error("function a(\n)")) assert.same({line = 1, column = 10, end_column = 10, msg = "expected identifier near '('"}, get_error("function (a)()")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function() end")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected '(' near 'a'"}, get_error("(function a() end)")) assert.same({line = 1, column = 18, end_column = 18, msg = "expected expression near ')'"}, get_error("function a() end()")) end) it("parses simple function with arguments correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}}, {}} } }, get_node("function a(b) end")) assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}, {tag = "Id", "c"}}, {}} } }, get_node("function a(b, c) end")) assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}, {tag = "Dots", "..."}}, {}} } }, get_node("function a(b, ...) end")) assert.same({line = 1, column = 15, end_column = 15, msg = "expected argument near ')'"}, get_error("function a(b, ) end")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected ')' near '.'"}, get_error("function a(b.c) end")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected ')' (to close '(' on line 1) near '.'"}, get_error("function a(\nb.c) end")) assert.same({line = 1, column = 12, end_column = 12, msg = "expected argument near '('"}, get_error("function a((b)) end")) assert.same({line = 1, column = 15, end_column = 15, msg = "expected ')' near ','"}, get_error("function a(..., ...) end")) end) it("parses field function correctly", function() assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Function", {}, {}} } }, get_node("function a.b() end")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Function", {}, {}} } }, get_node("function a.b.c() end")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected '(' near '['"}, get_error("function a[b]() end")) assert.same({line = 1, column = 12, end_column = 12, msg = "expected identifier near '('"}, get_error("function a.() end")) end) it("parses method function correctly", function() assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Function", {{tag = "Id", "self", implicit = true}}, {}} } }, get_node("function a:b() end")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Function", {{tag = "Id", "self", implicit = true}}, {}} } }, get_node("function a.b:c() end")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected '(' near '.'"}, get_error("function a:b.c() end")) end) end) describe("when parsing local declarations", function() it("parses simple local declaration correctly", function() assert.same({tag = "Local", { {tag = "Id", "a"} } }, get_node("local a")) assert.same({tag = "Local", { {tag = "Id", "a"}, {tag = "Id", "b"} } }, get_node("local a, b")) assert.same({line = 1, column = 6, end_column = 6, msg = "expected identifier near "}, get_error("local")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, get_error("local a,")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, get_error("local a.b")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, get_error("local a[b]")) assert.same({line = 1, column = 7, end_column = 7, msg = "expected identifier near '('"}, get_error("local (a)")) end) it("parses local declaration with assignment correctly", function() assert.same({tag = "Local", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node("local a = b")) assert.same({tag = "Local", { {tag = "Id", "a"}, {tag = "Id", "b"} }, { {tag = "Id", "c"}, {tag = "Id", "d"} } }, get_node("local a, b = c, d")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected expression near "}, get_error("local a = ")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected expression near "}, get_error("local a = b,")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, get_error("local a.b = c")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, get_error("local a[b] = c")) assert.same({line = 1, column = 10, end_column = 10, msg = "expected identifier near '('"}, get_error("local a, (b) = c")) end) it("parses local function declaration correctly", function() assert.same({tag = "Localrec", {tag = "Id", "a"}, {tag = "Function", {}, {}} }, get_node("local function a() end")) assert.same({line = 1, column = 15, end_column = 15, msg = "expected identifier near "}, get_error("local function")) assert.same({line = 1, column = 17, end_column = 17, msg = "expected '(' near '.'"}, get_error("local function a.b() end")) end) end) describe("when parsing assignments", function() it("parses single target assignment correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node("a = b")) assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Id", "c"} } }, get_node("a.b = c")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Id", "d"} } }, get_node("a.b.c = d")) assert.same({tag = "Set", { {tag = "Index", {tag = "Invoke", {tag = "Call", {tag = "Id", "f"}}, {tag = "String", "g"} }, {tag = "Number", "9"} } }, { {tag = "Id", "d"} } }, get_node("(f():g())[9] = d")) assert.same({line = 1, column = 2, end_column = 2, msg = "expected '=' near "}, get_error("a")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected expression near "}, get_error("a = ")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected statement near '='"}, get_error("a() = b")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '('"}, get_error("(a) = b")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '1'"}, get_error("1 = b")) end) it("parses multi assignment correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"}, {tag = "Id", "b"} }, { {tag = "Id", "c"}, {tag = "Id", "d"} } }, get_node("a, b = c, d")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected '=' near "}, get_error("a, b")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected identifier or field near '='"}, get_error("a, = b")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected expression near "}, get_error("a, b = ")) assert.same({line = 1, column = 10, end_column = 10, msg = "expected expression near "}, get_error("a, b = c,")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected call or indexing near '='"}, get_error("a, b() = c")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected identifier or field near '('"}, get_error("a, (b) = c")) end) end) describe("when parsing expression statements", function() it("parses calls correctly", function() assert.same({tag = "Call", {tag = "Id", "a"} }, get_node("a()")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "String", "b"} }, get_node("a'b'")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Table"} }, get_node("a{}")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} }, get_node("a(b)")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_node("a(b, c)")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} }, get_node("(a)(b)")) assert.same({tag = "Call", {tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} } }, get_node("(a)(b)()")) assert.same({line = 1, column = 2, end_column = 2, msg = "expected expression near ')'"}, get_error("()()")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected expression near "}, get_error("a(")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected ')' near "}, get_error("a(b")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected ')' (to close '(' on line 1) near "}, get_error("a(\nb")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '1'"}, get_error("1()")) assert.same({line = 1, column = 1, end_column = 5, msg = "expected statement near ''foo''"}, get_error("'foo'()")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function() end ()")) end) it("parses method calls correctly", function() assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"} }, get_node("a:b()")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "String", "c"} }, get_node("a:b'c'")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Table"} }, get_node("a:b{}")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"} }, get_node("a:b(c)")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"}, {tag = "Id", "d"} }, get_node("a:b(c, d)")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"} }, get_node("(a):b(c)")) assert.same({tag = "Invoke", {tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"} }, {tag = "String", "c"} }, get_node("a:b():c()")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '1'"}, get_error("1:b()")) assert.same({line = 1, column = 1, end_column = 2, msg = "expected statement near ''''"}, get_error("'':a()")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function()end:b()")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected method arguments near ':'"}, get_error("a:b:c()")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected identifier near "}, get_error("a:")) end) end) describe("when parsing expressions", function() it("parses singleton expressions correctly", function() assert.same({tag = "Nil"}, get_expr("nil")) assert.same({tag = "True"}, get_expr("true")) assert.same({tag = "False"}, get_expr("false")) assert.same({tag = "Number", "1"}, get_expr("1")) assert.same({tag = "String", "1"}, get_expr("'1'")) assert.same({tag = "Table"}, get_expr("{}")) assert.same({tag = "Function", {}, {}}, get_expr("function() end")) assert.same({tag = "Dots", "..."}, get_expr("...")) end) it("parses table constructors correctly", function() assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a, b, c}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Pair", {tag = "String", "b"}, {tag = "Id", "c"}}, {tag = "Id", "d"} }, get_expr("{a, b = c, d}")) assert.same({tag = "Table", {tag = "String", "a"}, {tag = "Pair", {tag = "Id", "b"}, {tag = "Id", "c"}}, {tag = "Id", "d"} }, get_expr("{[[a]], [b] = c, d}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c,}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c;}")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near ';'"}, get_error("return {;}")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near "}, get_error("return {")) assert.same({line = 1, column = 11, end_column = 13, msg = "expected '}' near 'end'"}, get_error("return {a end")) assert.same({line = 2, column = 1, end_column = 3, msg = "expected '}' (to close '{' on line 1) near 'end'"}, get_error("return {a\nend")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected ']' near "}, get_error("return {[a")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected ']' (to close '[' on line 1) near "}, get_error("return {[\na")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected expression near ','"}, get_error("return {a,,}")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected expression near "}, get_error("return {a = ")) end) it("wraps last element in table constructors in parens when needed", function() assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Paren", {tag = "Call", {tag = "Id", "f"} } } }, get_expr("{a, (f())}")) assert.same({tag = "Table", {tag = "Call", {tag = "Id", "f"} }, {tag = "Id", "a"} }, get_expr("{(f()), a}")) assert.same({tag = "Table", {tag = "Pair", {tag = "String", "a"}, {tag = "Call", {tag = "Id", "f"} } } }, get_expr("{a = (f())}")) assert.same({tag = "Table", {tag = "Call", {tag = "Id", "f"} }, {tag = "Pair", {tag = "String", "a"}, {tag = "Id", "b"} } }, get_expr("{(f()), a = b}")) end) it("parses simple expressions correctly", function() assert.same({tag = "Op", "unm", {tag = "Number", "1"} }, get_expr("-1")) assert.same({tag = "Op", "add", {tag = "Op", "add", {tag = "Number", "1"}, {tag = "Number", "2"} }, {tag = "Number", "3"} }, get_expr("1+2+3")) assert.same({tag = "Op", "pow", {tag = "Number", "1"}, {tag = "Op", "pow", {tag = "Number", "2"}, {tag = "Number", "3"} } }, get_expr("1^2^3")) assert.same({tag = "Op", "concat", {tag = "String", "1"}, {tag = "Op", "concat", {tag = "String", "2"}, {tag = "String", "3"} } }, get_expr("'1'..'2'..'3'")) end) it("handles operator precedence correctly", function() assert.same({tag = "Op", "add", {tag = "Op", "unm", {tag = "Number", "1"} }, {tag = "Op", "mul", {tag = "Number", "2"}, {tag = "Op", "pow", {tag = "Number", "3"}, {tag = "Number", "4"} } } }, get_expr("-1+2*3^4")) assert.same({tag = "Op", "bor", {tag = "Op", "bor", {tag = "Op", "band", {tag = "Op", "shr", {tag = "Number", "1"}, {tag = "Number", "2"} }, {tag = "Op", "shl", {tag = "Number", "3"}, {tag = "Number", "4"} } }, {tag = "Op", "bxor", {tag = "Number", "5"}, {tag = "Number", "6"} } }, {tag = "Op", "bnot", {tag = "Number", "7"} } }, get_expr("1 >> 2 & 3 << 4 | 5 ~ 6 | ~7")) assert.same({tag = "Op", "or", {tag = "Op", "and", {tag = "Op", "eq", {tag = "Id", "a"}, {tag = "Id", "b"} }, {tag = "Op", "eq", {tag = "Id", "c"}, {tag = "Id", "d"} } }, {tag = "Op", "ne", {tag = "Id", "e"}, {tag = "Id", "f"} } }, get_expr("a == b and c == d or e ~= f")) end) it("wraps last expression in a list in parens when needed", function() assert.same({tag = "Return", {tag = "Dots", "..."}, {tag = "Paren", {tag = "Dots", "..."}} }, get_node("return (...), (...)")) assert.same({tag = "Return", {tag = "Dots", "..."}, {tag = "Dots", "..."} }, get_node("return (...), ...")) assert.same({tag = "Return", {tag = "True"}, {tag = "False"} }, get_node("return (true), (false)")) assert.same({tag = "Return", {tag = "Call", {tag = "Id", "f"} }, {tag = "Paren", {tag = "Call", {tag = "Id", "g"} } } }, get_node("return (f()), (g())")) assert.same({tag = "Return", {tag = "Invoke", {tag = "Id", "f"}, {tag = "String", "n"} }, {tag = "Paren", {tag = "Invoke", {tag = "Id", "g"}, {tag = "String", "m"} } } }, get_node("return (f:n()), (g:m())")) end) end) describe("when parsing multiple statements", function() it("considers semicolons and comments no-op statements", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node(";;;a = b;--[[]];--;")) end) it("does not allow statements after return", function() assert.same({line = 1, column = 8, end_column = 12, msg = "expected expression near 'break'"}, get_error("return break")) assert.same({line = 1, column = 9, end_column = 13, msg = "expected end of block near 'break'"}, get_error("return; break")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected end of block near ';'"}, get_error("return;;")) assert.same({line = 1, column = 10, end_column = 14, msg = "expected end of block near 'break'"}, get_error("return 1 break")) assert.same({line = 1, column = 11, end_column = 15, msg = "expected end of block near 'break'"}, get_error("return 1; break")) assert.same({line = 1, column = 13, end_column = 17, msg = "expected end of block near 'break'"}, get_error("return 1, 2 break")) assert.same({line = 1, column = 14, end_column = 18, msg = "expected end of block near 'break'"}, get_error("return 1, 2; break")) end) it("parses nested statements correctly", function() assert.same({ {tag = "Localrec", {tag = "Id", "f"}, {tag = "Function", {}, { {tag = "While", {tag = "True"}, { {tag = "If", {tag = "Nil"}, { {tag = "Call", {tag = "Id", "f"} }, {tag = "Return"} }, {tag = "False"}, { {tag = "Call", {tag = "Id", "g"} }, {tag = "Break"} }, { {tag = "Call", {tag = "Id", "h"} }, {tag = "Repeat", { {tag = "Goto", "fail"} }, {tag = "Id", "get_forked"} } } } } }, {tag = "Label", "fail"} }} }, {tag = "Do", {tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Number", "2"}, { {tag = "Call", {tag = "Id", "nothing"} } } }, {tag = "Forin", { {tag = "Id", "k"}, {tag = "Id", "v"} }, { {tag = "Call", {tag = "Id", "pairs"} } }, { {tag = "Call", {tag = "Id", "print"}, {tag = "String", "bar"} }, {tag = "Call", {tag = "Id", "assert"}, {tag = "Number", "42"} } } }, {tag = "Return"} }, }, get_ast([[ local function f() while true do if nil then f() return elseif false then g() break else h() repeat goto fail until get_forked end end ::fail:: end do for i=1, 2 do nothing() end for k, v in pairs() do print("bar") assert(42) end return end ]])) end) end) it("provides correct location info", function() assert.same({ {tag = "Localrec", location = {line = 1, column = 1, offset = 1}, first_token = "local", {tag = "Id", "foo", location = {line = 1, column = 16, offset = 16}}, {tag = "Function", location = {line = 1, column = 7, offset = 7}, end_location = {line = 4, column = 1, offset = 78}, { {tag = "Id", "a", location = {line = 1, column = 20, offset = 20}}, {tag = "Id", "b", location = {line = 1, column = 23, offset = 23}}, {tag = "Id", "c", location = {line = 1, column = 26, offset = 26}}, {tag = "Dots", "...", location = {line = 1, column = 29, offset = 29}} }, { {tag = "Local", location = {line = 2, column = 4, offset = 37}, first_token = "local", equals_location = {line = 2, column = 12, offset = 45}, { {tag = "Id", "d", location = {line = 2, column = 10, offset = 43}} }, { {tag = "Op", "mul", location = {line = 2, column = 15, offset = 48}, {tag = "Op", "add", location = {line = 2, column = 15, offset = 48}, {tag = "Id", "a", location = {line = 2, column = 15, offset = 48}}, {tag = "Id", "b", location = {line = 2, column = 19, offset = 52}} }, {tag = "Id", "c", location = {line = 2, column = 24, offset = 57}} } } }, {tag = "Return", location = {line = 3, column = 4, offset = 62}, first_token = "return", {tag = "Id", "d", location = {line = 3, column = 11, offset = 69}}, {tag = "Paren", location = {line = 3, column = 15, offset = 73}, {tag = "Dots", "...", location = {line = 3, column = 15, offset = 73}} } } } } }, {tag = "Set", location = {line = 6, column = 1, offset = 83}, first_token = "function", { {tag = "Index", location = {line = 6, column = 10, offset = 92}, {tag = "Id", "t", location = {line = 6, column = 10, offset = 92}}, {tag = "String", "bar", location = {line = 6, column = 12, offset = 94}} } }, { {tag = "Function", location = {line = 6, column = 1, offset = 83}, end_location = {line = 10, column = 1, offset = 142}, { {tag = "Id", "self", implicit = true, location = {line = 6, column = 11, offset = 93}}, {tag = "Id", "arg", location = {line = 6, column = 16, offset = 98}} }, { {tag = "If", location = {line = 7, column = 4, offset = 106}, first_token = "if", {tag = "Id", "arg", location = {line = 7, column = 7, offset = 109}, first_token = "arg"}, {location = {line = 7, column = 11, offset = 113}, -- Branch location. {tag = "Call", location = {line = 8, column = 7, offset = 124}, first_token = "print", {tag = "Id", "print", location = {line = 8, column = 7, offset = 124}}, {tag = "Id", "arg", location = {line = 8, column = 13, offset = 130}} } } } } } } } }, (parser([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) end function t:bar(arg) if arg then print(arg) end end ]]))) end) it("provides correct location info for labels", function() assert.same({ {tag = "Label", "foo", location = {line = 1, column = 1, offset = 1}, end_column = 7, first_token = "::"}, {tag = "Label", "bar", location = {line = 2, column = 1, offset = 9}, end_column = 6, first_token = "::"}, {tag = "Label", "baz", location = {line = 3, column = 3, offset = 18}, end_column = 4, first_token = "::"} }, (parser([[ ::foo:: :: bar :::: baz:: ]]))) end) it("provides correct location info for statements starting with expressions", function() assert.same({ {tag = "Call", location = {line = 1, column = 1, offset = 1}, first_token = "a", {tag = "Id", "a", location = {line = 1, column = 1, offset = 1}} }, {tag = "Call", location = {line = 2, column = 1, offset = 6}, first_token = "(", {tag = "Id", "b", location = {line = 2, column = 2, offset = 7}} }, {tag = "Set", location = {line = 3, column = 1, offset = 13}, first_token = "(", equals_location = {line = 3, column = 12, offset = 24}, { {tag = "Index", location = {line = 3, column = 3, offset = 15}, {tag = "Index", location = {line = 3, column = 3, offset = 15}, {tag = "Id", "c", location = {line = 3, column = 3, offset = 15}}, {tag = "String", "d", location = {line = 3, column = 6, offset = 18}} }, {tag = "Number", "3", location = {line = 3, column = 9, offset = 21}} } }, { {tag = "Number", "2", location = {line = 3, column = 14, offset = 26}} } } }, (parser([[ a(); (b)(); ((c).d)[3] = 2 ]]))) end) it("provides correct error location info", function() assert.same({line = 8, column = 15, end_column = 15, msg = "expected '=' near ')'"}, get_error([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) end function t:bar(arg) if arg then printarg) end end ]])) end) describe("providing misc information", function() it("provides comments correctly", function() assert.same({ {contents = " ignore something", location = {line = 1, column = 1, offset = 1}, end_column = 19}, {contents = " comments", location = {line = 2, column = 13, offset = 33}, end_column = 23}, {contents = "long comment", location = {line = 3, column = 13, offset = 57}, end_column = 17} }, get_comments([[ -- ignore something foo = bar() -- comments return true --[=[ long comment]=] ]])) end) it("provides lines with code correctly", function() -- EOS is considered "code" (which does not matter w.r.t inline options). assert.same({nil, true, true, true, true, true, nil, nil, true, true, true}, get_code_lines([[ -- nothing here local foo = 2 + 3 + { --[=[empty]=] } ::bar:: ]])) end) end) end) luacheck-0.13.0/spec/folder/0000755000175000017500000000000012642521554014617 5ustar vsevavsevaluacheck-0.13.0/spec/folder/bom0000644000175000017500000000001312642521554015311 0ustar vsevavsevafoo bar luacheck-0.13.0/spec/folder/bad_rockspec0000644000175000017500000000002012642521554017151 0ustar vsevavsevabuild "builtin" luacheck-0.13.0/spec/folder/config0000644000175000017500000000001412642521554016002 0ustar vsevavsevafoo = "bar" luacheck-0.13.0/spec/folder/folder1/0000755000175000017500000000000012642521554016153 5ustar vsevavsevaluacheck-0.13.0/spec/folder/folder1/fail0000644000175000017500000000000012642521554016777 0ustar vsevavsevaluacheck-0.13.0/spec/folder/folder1/file0000644000175000017500000000000012642521554017003 0ustar vsevavsevaluacheck-0.13.0/spec/folder/folder1/another0000644000175000017500000000000012642521554017524 0ustar vsevavsevaluacheck-0.13.0/spec/folder/rockspec0000644000175000017500000000035112642521554016352 0ustar vsevavsevabuild = { type = "builtin", modules = { foo = "foo.lua", bar = "bar.lua", qu = "qu.c" }, install = { lua = { baz = "baz.lua" }, bin = { bin = "bin.lua" } } } luacheck-0.13.0/spec/folder/folder2/0000755000175000017500000000000012642521554016154 5ustar vsevavsevaluacheck-0.13.0/spec/folder/folder2/garbage0000644000175000017500000000000012642521554017455 0ustar vsevavsevaluacheck-0.13.0/spec/folder/foo0000644000175000017500000000001112642521554015315 0ustar vsevavsevacontents luacheck-0.13.0/spec/folder/bad_config0000644000175000017500000000001412642521554016610 0ustar vsevavsevafoo = `bar` luacheck-0.13.0/spec/folder/env_config0000644000175000017500000000001412642521554016652 0ustar vsevavsevafoo = bar() luacheck-0.13.0/spec/samples/0000755000175000017500000000000012642521554015010 5ustar vsevavsevaluacheck-0.13.0/spec/samples/python_code.lua0000644000175000017500000000003712642521554020026 0ustar vsevavsevafrom __future__ import braces luacheck-0.13.0/spec/samples/redefined.lua0000644000175000017500000000026412642521554017442 0ustar vsevavsevalocal a = {} function a:b(...) local a, self = 4 do local a = (...)(a) each(a, function() local self = self[5]; return self.bar end) end print(a[1]) end luacheck-0.13.0/spec/samples/sample.rockspec0000644000175000017500000000021012642521554020015 0ustar vsevavsevabuild = { type = "builtin", modules = { good = "spec/samples/good_code.lua", bad = "spec/samples/bad_code.lua" } } luacheck-0.13.0/spec/samples/unused_code.lua0000644000175000017500000000042012642521554020004 0ustar vsevavsevalocal foo = {} function foo.bar(baz) for i=1, 5 do local q for a, b, c in pairs(foo) do print(4) end end end local x = 5 x = 6 x = 7; print(x) local y = 5; (function() print(y) end)() y = 6 local z = 5; (function() z = 4 end)() z = 6 luacheck-0.13.0/spec/samples/empty.lua0000644000175000017500000000000012642521554016637 0ustar vsevavsevaluacheck-0.13.0/spec/samples/defined.lua0000644000175000017500000000005212642521554017106 0ustar vsevavsevafoo = {} function foo.bar() baz() end luacheck-0.13.0/spec/samples/global_inline_options.lua0000644000175000017500000000043712642521554022070 0ustar vsevavseva-- luacheck: allow defined top foo = 4 print(foo) bar = 6 -- luacheck: ignore 131 function f() baz = 5 -- luacheck: allow defined qu = 4 print(qu) end -- luacheck: module, globals external quu = 7 print(external) local function g() -- luacheck: ignore external = 8 end luacheck-0.13.0/spec/samples/defined2.lua0000644000175000017500000000001212642521554017164 0ustar vsevavsevafoo.bar() luacheck-0.13.0/spec/samples/inline_options.lua0000644000175000017500000000104212642521554020541 0ustar vsevavseva-- luacheck: ignore 4 -- luacheck: ignore foo bar foo() bar() local function f(a) -- luacheck: no unused args -- luacheck: globals baz foo() bar() baz() qu() -- luacheck: globals qu qu() end baz() -- luacheck should ignore this comment -- luacheck: push ignore 2/f local f -- luacheck: push ignore 2/g local g -- luacheck: pop local f, g -- luacheck: pop local f, g -- luacheck: push local function f() --luacheck: ignore -- luacheck: pop end -- luacheck: ignore 5 do end -- luacheck: enable 54 do end if false then end luacheck-0.13.0/spec/samples/good_code.lua0000644000175000017500000000025512642521554017437 0ustar vsevavsevalocal embracer = {} local function helper() -- NYI wontfix end function embracer.embrace(opt) opt = opt or "default" return helper(opt.."?") end return embracer luacheck-0.13.0/spec/samples/bad.rockspec0000644000175000017500000000001212642521554017262 0ustar vsevavsevabuild = 0 luacheck-0.13.0/spec/samples/bad_code.lua0000644000175000017500000000023212642521554017230 0ustar vsevavsevapackage.loaded[...] = {} local function helper(...) -- NYI end function embrace(opt) local opt = opt or "default" return hepler(opt.."?") end luacheck-0.13.0/spec/samples/defined3.lua0000644000175000017500000000003312642521554017170 0ustar vsevavsevafoo = {} foo = {} bar = {} luacheck-0.13.0/spec/samples/globals.lua0000644000175000017500000000004112642521554017131 0ustar vsevavsevaprint(setfenv(rawlen(tostring))) luacheck-0.13.0/spec/samples/compat.lua0000644000175000017500000000005112642521554016772 0ustar vsevavseva(setfenv and rawlen)(setfenv and rawlen) luacheck-0.13.0/spec/samples/defined4.lua0000644000175000017500000000005212642521554017172 0ustar vsevavsevafunction foo() foo = 1 bar = {} end luacheck-0.13.0/spec/samples/read_globals.lua0000644000175000017500000000017312642521554020132 0ustar vsevavsevastring = "foo" table.append = table.insert _ENV = nil foo = "4"; print(foo) bar = "5"; print(bar) baz[4] = "6"; print(baz) luacheck-0.13.0/spec/samples/custom_std_inline_options.lua0000644000175000017500000000022112642521554023003 0ustar vsevavseva-- luacheck: push -- luacheck: std +busted tostring(setfenv, print(it)) -- luacheck: pop -- luacheck: std other_std tostring(setfenv, print(it)) luacheck-0.13.0/spec/samples/unused_secondaries.lua0000644000175000017500000000020012642521554021365 0ustar vsevavsevalocal function f() end local a, b = f() f(b) local x, y, z = f(), f() f(y) local t, o = {} o = 5 print(o) o, t[1] = f() f(t) luacheck-0.13.0/spec/samples/bad_flow.lua0000644000175000017500000000067412642521554017277 0ustar vsevavsevaif io.read("*n") == "exit" then else local a, b do -- print(something) end for _ = 1, 10 do if io.read("*n") == "foobar" then a, b = 1 print(a, b) break else a, b = 1, 2, 3 print(a, b) break end print("How could this happen?") end end while true do if package.loaded.foo then return 4 else print(5) break end end luacheck-0.13.0/spec/samples/argparse.lua0000644000175000017500000005750112642521554017327 0ustar vsevavsevalocal Parser, Command, Argument, Option do -- Create classes with setters local class = require "30log" local function add_setters(cl, fields) for field, setter in pairs(fields) do cl[field] = function(self, value) setter(self, value) self["_"..field] = value return self end end cl.__init = function(self, ...) return self(...) end cl.__call = function(self, ...) local name_or_options for i=1, select("#", ...) do name_or_options = select(i, ...) if type(name_or_options) == "string" then if self._aliases then table.insert(self._aliases, name_or_options) end if not self._aliases or not self._name then self._name = name_or_options end elseif type(name_or_options) == "table" then for field, setter in pairs(fields) do if name_or_options[field] ~= nil then self[field](self, name_or_options[field]) end end end end return self end return cl end local typecheck = setmetatable({}, { __index = function(self, type_) local typechecker_factory = function(field) return function(_, value) if type(value) ~= type_ then error(("bad field '%s' (%s expected, got %s)"):format(field, type_, type(value))) end end end self[type_] = typechecker_factory return typechecker_factory end }) local function aliased_name(self, name) typecheck.string "name" (self, name) table.insert(self._aliases, name) end local function aliased_aliases(self, aliases) typecheck.table "aliases" (self, aliases) if not self._name then self._name = aliases[1] end end local function parse_boundaries(boundaries) if tonumber(boundaries) then return tonumber(boundaries), tonumber(boundaries) end if boundaries == "*" then return 0, math.huge end if boundaries == "+" then return 1, math.huge end if boundaries == "?" then return 0, 1 end if boundaries:match "^%d+%-%d+$" then local min, max = boundaries:match "^(%d+)%-(%d+)$" return tonumber(min), tonumber(max) end if boundaries:match "^%d+%+$" then local min = boundaries:match "^(%d+)%+$" return tonumber(min), math.huge end end local function boundaries(field) return function(self, value) local min, max = parse_boundaries(value) if not min then error(("bad field '%s'"):format(field)) end self["_min"..field], self["_max"..field] = min, max end end local function convert(self, value) if type(value) ~= "function" then if type(value) ~= "table" then error(("bad field 'convert' (function or table expected, got %s)"):format(type(value))) end end end local function argname(self, value) if type(value) ~= "string" then if type(value) ~= "table" then error(("bad field 'argname' (string or table expected, got %s)"):format(type(value))) end end end local function add_help(self, param) if self._has_help then table.remove(self._options) self._has_help = false end if param then local help = self:flag() :description "Show this help message and exit. " :action(function() io.stdout:write(self:get_help() .. "\r\n") os.exit(0) end)(param) if not help._name then help "-h" "--help" end self._has_help = true end end Parser = add_setters(class { __name = "Parser", _arguments = {}, _options = {}, _commands = {}, _mutexes = {}, _require_command = true }, { name = typecheck.string "name", description = typecheck.string "description", epilog = typecheck.string "epilog", require_command = typecheck.boolean "require_command", usage = typecheck.string "usage", help = typecheck.string "help", add_help = add_help }) Command = add_setters(Parser:extends { __name = "Command", _aliases = {} }, { name = aliased_name, aliases = aliased_aliases, description = typecheck.string "description", epilog = typecheck.string "epilog", target = typecheck.string "target", require_command = typecheck.boolean "require_command", action = typecheck["function"] "action", usage = typecheck.string "usage", help = typecheck.string "help", add_help = add_help }) Argument = add_setters(class { __name = "Argument", _minargs = 1, _maxargs = 1, _mincount = 1, _maxcount = 1, _defmode = "unused" }, { name = typecheck.string "name", description = typecheck.string "description", target = typecheck.string "target", args = boundaries "args", default = typecheck.string "default", defmode = typecheck.string "defmode", convert = convert, argname = argname }) Option = add_setters(Argument:extends { __name = "Option", _aliases = {}, _mincount = 0, _overwrite = true }, { name = aliased_name, aliases = aliased_aliases, description = typecheck.string "description", target = typecheck.string "target", args = boundaries "args", count = boundaries "count", default = typecheck.string "default", defmode = typecheck.string "defmode", convert = convert, overwrite = typecheck.boolean "overwrite", action = typecheck["function"] "action", argname = argname }) end function Argument:_get_argument_list() local buf = {} local i = 1 while i <= math.min(self._minargs, 3) do local argname = self:_get_argname_i(i) if self._default and self._defmode:find "a" then argname = "[" .. argname .. "]" end table.insert(buf, argname) i = i+1 end while i <= math.min(self._maxargs, 3) do table.insert(buf, "[" .. self:_get_argname_i(i) .. "]") i = i+1 if self._maxargs == math.huge then break end end if i < self._maxargs then table.insert(buf, "...") end return buf end function Argument:_get_usage() local usage = table.concat(self:_get_argument_list(), " ") if self._default and self._defmode:find "u" then if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then usage = "[" .. usage .. "]" end end return usage end function Argument:_get_type() if self._maxcount == 1 then if self._maxargs == 0 then return "flag" elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then return "arg" else return "multiarg" end else if self._maxargs == 0 then return "counter" elseif self._maxargs == 1 and self._minargs == 1 then return "multicount" else return "twodimensional" end end end function Argument:_get_argname_i(i) local argname = self:_get_argname() if type(argname) == "table" then return argname[i] else return argname end end function Argument:_get_argname() return self._argname or ("<"..self._name..">") end function Option:_get_argname() return self._argname or ("<"..self:_get_target()..">") end function Argument:_get_label() return self._name end function Option:_get_label() local variants = {} local argument_list = self:_get_argument_list() table.insert(argument_list, 1, nil) for _, alias in ipairs(self._aliases) do argument_list[1] = alias table.insert(variants, table.concat(argument_list, " ")) end return table.concat(variants, ", ") end function Command:_get_label() return table.concat(self._aliases, ", ") end function Argument:_get_description() if self._default then if self._description then return ("%s (default: %s)"):format(self._description, self._default) else return ("default: %s"):format(self._default) end else return self._description or "" end end function Command:_get_description() return self._description or "" end function Option:_get_usage() local usage = self:_get_argument_list() table.insert(usage, 1, self._name) usage = table.concat(usage, " ") if self._mincount == 0 or self._default then usage = "[" .. usage .. "]" end return usage end function Option:_get_target() if self._target then return self._target end for _, alias in ipairs(self._aliases) do if alias:sub(1, 1) == alias:sub(2, 2) then return alias:sub(3) end end return self._name:sub(2) end function Parser:_get_fullname() local parent = self._parent local buf = {self._name} while parent do table.insert(buf, 1, parent._name) parent = parent._parent end return table.concat(buf, " ") end function Parser:_update_charset(charset) charset = charset or {} for _, command in ipairs(self._commands) do command:_update_charset(charset) end for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do charset[alias:sub(1, 1)] = true end end return charset end function Parser:argument(...) local argument = Argument:new(...) table.insert(self._arguments, argument) return argument end function Parser:option(...) local option = Option:new(...) if self._has_help then table.insert(self._options, #self._options, option) else table.insert(self._options, option) end return option end function Parser:flag(...) return self:option():args(0)(...) end function Parser:command(...) local command = Command:new():add_help(true)(...) command._parent = self table.insert(self._commands, command) return command end function Parser:mutex(...) local options = {...} for i, option in ipairs(options) do assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i)) end table.insert(self._mutexes, options) return self end local max_usage_width = 70 local usage_welcome = "Usage: " function Parser:get_usage() if self._usage then return self._usage end local lines = {usage_welcome .. self:_get_fullname()} local function add(s) if #lines[#lines]+1+#s <= max_usage_width then lines[#lines] = lines[#lines] .. " " .. s else lines[#lines+1] = (" "):rep(#usage_welcome) .. s end end -- set of mentioned elements local used = {} for _, mutex in ipairs(self._mutexes) do local buf = {} for _, option in ipairs(mutex) do table.insert(buf, option:_get_usage()) used[option] = true end add("(" .. table.concat(buf, " | ") .. ")") end for _, elements in ipairs{self._options, self._arguments} do for _, element in ipairs(elements) do if not used[element] then add(element:_get_usage()) end end end if #self._commands > 0 then if self._require_command then add("") else add("[]") end add("...") end return table.concat(lines, "\r\n") end local margin_len = 3 local margin_len2 = 25 local margin = (" "):rep(margin_len) local margin2 = (" "):rep(margin_len2) local function make_two_columns(s1, s2) if s2 == "" then return margin .. s1 end s2 = s2:gsub("[\r\n][\r\n]?", function(sub) if #sub == 1 or sub == "\r\n" then return "\r\n" .. margin2 else return "\r\n\r\n" .. margin2 end end) if #s1 < (margin_len2-margin_len) then return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2 else return margin .. s1 .. "\r\n" .. margin2 .. s2 end end function Parser:get_help() if self._help then return self._help end local blocks = {self:get_usage()} if self._description then table.insert(blocks, self._description) end local labels = {"Arguments: ", "Options: ", "Commands: "} for i, elements in ipairs{self._arguments, self._options, self._commands} do if #elements > 0 then local buf = {labels[i]} for _, element in ipairs(elements) do table.insert(buf, make_two_columns(element:_get_label(), element:_get_description())) end table.insert(blocks, table.concat(buf, "\r\n")) end end if self._epilog then table.insert(blocks, self._epilog) end return table.concat(blocks, "\r\n\r\n") end local function get_tip(context, wrong_name) local context_pool = {} local possible_name local possible_names = {} for name in pairs(context) do for i=1, #name do possible_name = name:sub(1, i-1) .. name:sub(i+1) if not context_pool[possible_name] then context_pool[possible_name] = {} end table.insert(context_pool[possible_name], name) end end for i=1, #wrong_name+1 do possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1) if context[possible_name] then possible_names[possible_name] = true elseif context_pool[possible_name] then for _, name in ipairs(context_pool[possible_name]) do possible_names[name] = true end end end local first = next(possible_names) if first then if next(possible_names, first) then local possible_names_arr = {} for name in pairs(possible_names) do table.insert(possible_names_arr, "'" .. name .. "'") end table.sort(possible_names_arr) return "\r\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" else return "\r\nDid you mean '" .. first .. "'?" end else return "" end end local function plural(x) if x == 1 then return "" end return "s" end local default_cmdline = arg or {} function Parser:_parse(args, errhandler) args = args or default_cmdline local parser local charset local options = {} local arguments = {} local commands local option_mutexes = {} local used_mutexes = {} local opt_context = {} local com_context local result = {} local invocations = {} local passed = {} local cur_option local cur_arg_i = 1 local cur_arg local targets = {} local function error_(fmt, ...) return errhandler(parser, fmt:format(...)) end local function assert_(assertion, ...) return assertion or error_(...) end local function convert(element, data) if element._convert then local ok, err if type(element._convert) == "function" then ok, err = element._convert(data) else ok, err = element._convert[data] end assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'") data = ok end return data end local invoke, pass, close function invoke(element) local overwrite = false if invocations[element] == element._maxcount then if element._overwrite then overwrite = true else error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount)) end else invocations[element] = invocations[element]+1 end passed[element] = 0 local type_ = element:_get_type() local target = targets[element] if type_ == "flag" then result[target] = true elseif type_ == "multiarg" then result[target] = {} elseif type_ == "counter" then if not overwrite then result[target] = result[target]+1 end elseif type_ == "multicount" then if overwrite then table.remove(result[target], 1) end elseif type_ == "twodimensional" then table.insert(result[target], {}) if overwrite then table.remove(result[target], 1) end end if element._maxargs == 0 then close(element) end end function pass(element, data) passed[element] = passed[element]+1 data = convert(element, data) local type_ = element:_get_type() local target = targets[element] if type_ == "arg" then result[target] = data elseif type_ == "multiarg" or type_ == "multicount" then table.insert(result[target], data) elseif type_ == "twodimensional" then table.insert(result[target][#result[target]], data) end if passed[element] == element._maxargs then close(element) end end local function complete_invocation(element) while passed[element] < element._minargs do pass(element, element._default) end end function close(element) if passed[element] < element._minargs then if element._default and element._defmode:find "a" then complete_invocation(element) else error_("too few arguments") end else if element == cur_option then cur_option = nil elseif element == cur_arg then cur_arg_i = cur_arg_i+1 cur_arg = arguments[cur_arg_i] end end end local function switch(p) parser = p for _, option in ipairs(parser._options) do table.insert(options, option) for _, alias in ipairs(option._aliases) do opt_context[alias] = option end local type_ = option:_get_type() targets[option] = option:_get_target() if type_ == "counter" then result[targets[option]] = 0 elseif type_ == "multicount" or type_ == "twodimensional" then result[targets[option]] = {} end invocations[option] = 0 end for _, mutex in ipairs(parser._mutexes) do for _, option in ipairs(mutex) do if not option_mutexes[option] then option_mutexes[option] = {mutex} else table.insert(option_mutexes[option], mutex) end end end for _, argument in ipairs(parser._arguments) do table.insert(arguments, argument) invocations[argument] = 0 targets[argument] = argument._target or argument._name invoke(argument) end cur_arg = arguments[cur_arg_i] commands = parser._commands com_context = {} for _, command in ipairs(commands) do targets[command] = command._target or command._name for _, alias in ipairs(command._aliases) do com_context[alias] = command end end end local function get_option(name) return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name)) end local function do_action(element) if element._action then element._action() end end local function handle_argument(data) if cur_option then pass(cur_option, data) elseif cur_arg then pass(cur_arg, data) else local com = com_context[data] if not com then if #commands > 0 then error_("unknown command '%s'%s", data, get_tip(com_context, data)) else error_("too many arguments") end else result[targets[com]] = true do_action(com) switch(com) end end end local function handle_option(data) if cur_option then close(cur_option) end cur_option = opt_context[data] if option_mutexes[cur_option] then for _, mutex in ipairs(option_mutexes[cur_option]) do if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name) else used_mutexes[mutex] = cur_option end end end do_action(cur_option) invoke(cur_option) end local function mainloop() local handle_options = true for _, data in ipairs(args) do local plain = true local first, name, option if handle_options then first = data:sub(1, 1) if charset[first] then if #data > 1 then plain = false if data:sub(2, 2) == first then if #data == 2 then if cur_option then close(cur_option) end handle_options = false else local equal = data:find "=" if equal then name = data:sub(1, equal-1) option = get_option(name) assert_(option._maxargs > 0, "option '%s' does not take arguments", name) handle_option(data:sub(1, equal-1)) handle_argument(data:sub(equal+1)) else get_option(data) handle_option(data) end end else for i = 2, #data do name = first .. data:sub(i, i) option = get_option(name) handle_option(name) if i ~= #data and option._minargs > 0 then handle_argument(data:sub(i+1)) break end end end end end end if plain then handle_argument(data) end end end switch(self) charset = parser:_update_charset() mainloop() if cur_option then close(cur_option) end while cur_arg do if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then complete_invocation(cur_arg) else close(cur_arg) end end if parser._require_command and #commands > 0 then error_("a command is required") end for _, option in ipairs(options) do if invocations[option] == 0 then if option._default and option._defmode:find "u" then invoke(option) complete_invocation(option) close(option) end end if invocations[option] < option._mincount then if option._default and option._defmode:find "a" then while invocations[option] < option._mincount do invoke(option) close(option) end else error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount)) end end end return result end function Parser:error(msg) if _TEST then error(msg) else io.stderr:write(("%s\r\n\r\nError: %s\r\n"):format(self:get_usage(), msg)) os.exit(1) end end function Parser:parse(args) return self:_parse(args, Parser.error) end function Parser:pparse(args) local errmsg local ok, result = pcall(function() return self:_parse(args, function(parser, err) errmsg = err return error() end) end) if ok then return true, result else assert(errmsg, result) return false, errmsg end end return function(...) return Parser(default_cmdline[0]):add_help(true)(...) end luacheck-0.13.0/spec/samples/read_globals_inline_options.lua0000644000175000017500000000027312642521554023244 0ustar vsevavseva-- luacheck: read globals foo bar foo(bar, baz) foo, bar, baz, baz[1] = false, true, nil, 5 -- luacheck: ignore 111/foo 121/ba. -- luacheck: globals bar baz foo, bar, baz = 678, 829, 914 luacheck-0.13.0/spec/fs_spec.lua0000644000175000017500000000457412642521554015503 0ustar vsevavsevalocal fs = require "luacheck.fs" describe("fs", function() describe("is_dir", function() it("returns true for directories", function() assert.is_true(fs.is_dir("spec/folder")) end) it("returns false for files", function() assert.is_false(fs.is_dir("spec/folder/foo")) end) it("returns false for non-existent paths", function() assert.is_false(fs.is_dir("spec/folder/non-existent")) end) end) describe("is_file", function() it("returns true for files", function() assert.is_true(fs.is_file("spec/folder/foo")) end) it("returns false for directories", function() assert.is_false(fs.is_file("spec/folder")) end) it("returns false for non-existent paths", function() assert.is_false(fs.is_file("spec/folder/non-existent")) end) end) describe("extract_files", function() it("returns sorted list of files in a directory matching pattern", function() assert.same({ "spec/folder/folder1/fail", "spec/folder/folder1/file", "spec/folder/foo" }, fs.extract_files("spec/folder", "^f")) end) end) describe("mtime", function() it("returns modification time as a number", function() assert.number(fs.mtime("spec/folder/foo")) end) it("returns nil for non-existent files", function() assert.is_nil(fs.mtime("spec/folder/non-existent")) end) end) describe("current_dir", function() it("returns absolute path to current directory", function() local current_dir = fs.current_dir() assert.string(current_dir) assert.equal("/", current_dir:sub(1, 1)) assert.is_true(fs.is_file(current_dir .. "spec/folder/foo")) end) end) describe("find_file", function() it("finds file in a directory", function() local path = fs.current_dir() .. "spec/folder" assert.equal(path, fs.find_file(path, "foo")) end) it("finds file in a parent directory", function() local path = fs.current_dir() .. "spec/folder" assert.equal(path, fs.find_file(path .. "/folder1", "foo")) end) it("returns nil if can't find file", function() assert.is_nil(fs.find_file(fs.current_dir(), "this file shouldn't exist or it will make luacheck testsuite break")) end) end) end) luacheck-0.13.0/spec/analyze_spec.lua0000644000175000017500000000705012642521554016526 0ustar vsevavsevalocal analyze = require "luacheck.analyze" local linearize = require "luacheck.linearize" local parser = require "luacheck.parser" local utils = require "luacheck.utils" local ChState = utils.class() function ChState.__init() end function ChState.warn_redefined() end function ChState.warn_global() end function ChState.warn_unused_label() end function ChState.warn_unused_variable() end function ChState.warn_unused_value() end function ChState.warn_unset() end local function get_line_(src) local ast = parser(src) local chstate = ChState() return linearize(chstate, ast) end local function get_line(src) local ok, res = pcall(get_line_, src) if ok then return res elseif type(res) == "table" then return nil else error(res, 0) end end local function used_variables_to_string(item) local buf = {} for var, values in pairs(item.used_values) do local values_buf = {} for _, value in ipairs(values) do table.insert(values_buf, tostring(value.location.line) .. ":" .. tostring(value.location.column)) end table.insert(buf, var.name .. " = (" .. table.concat(values_buf, ", ") .. ")") end table.sort(buf) return item.tag .. ": " .. table.concat(buf, "; ") end local function get_used_variables_as_string(src) local line = get_line(src) analyze(ChState(), line) local buf = {} for _, item in ipairs(line.items) do if item.accesses and next(item.accesses) then assert.is_table(item.used_values) table.insert(buf, used_variables_to_string(item)) end end return table.concat(buf, "\n") end describe("analyze", function() describe("when resolving values", function() it("resolves values in linear cases", function() assert.equal([[ Eval: a = (1:7)]], get_used_variables_as_string([[ local a = 6 print(a) ]])) end) it("resolves values after ifs", function() assert.equal([[ Eval: a = (1:7, 4:4)]], get_used_variables_as_string([[ local a if expr then a = 5 end print(a) ]])) assert.equal([[ Eval: a = (4:4, 7:4, 10:7, 13:4)]], get_used_variables_as_string([[ local a = 3 if expr then a = 4 elseif expr then a = 5 a = 8 if expr then a = 7 end else a = 6 end print(a) ]])) end) it("resolves values after loops", function() assert.equal([[ Eval: a = (1:7, 5:7) Eval: a = (1:7, 5:7)]], get_used_variables_as_string([[ local a while not a do if expr then a = expr2 end end print(a) ]])) assert.equal([[ Set: k = (2:5) Eval: v = (2:8) Eval: a = (3:4); b = (1:10) Eval: a = (1:7, 3:4); b = (1:10)]], get_used_variables_as_string([[ local a, b = 1, 2 for k, v in pairs(t) do a = k if v then print(a, b) end end print(a, b) ]])) end) end) describe("when resolving upvalues", function() it("resolves set upvalues naively", function() assert.equal([[ Eval: f = (3:16) Eval: a = (1:7, 4:4)]], get_used_variables_as_string([[ local a local function f() a = 5 end f() print(a) ]])) end) it("naively determines where closure is live", function() assert.equal([[ Eval: a = (1:7) Eval: a = (1:7, 6:4)]], get_used_variables_as_string([[ local a = 4 print(a) local function f() a = 5 end print(a) ]])) end) it("naively determines where closure is live in loops", function() assert.equal([[ Eval: a = (1:7, 6:22) Eval: a = (1:7, 6:22)]], get_used_variables_as_string([[ local a = 4 repeat print(a) escape(function() a = 5 end) until a ]])) end) end) end) luacheck-0.13.0/spec/caches/0000755000175000017500000000000012642521554014572 5ustar vsevavsevaluacheck-0.13.0/spec/caches/old_format.cache0000644000175000017500000000002212642521554017677 0ustar vsevavsevafoo 123 return {} luacheck-0.13.0/spec/caches/different_format.cache0000644000175000017500000000002612642521554021073 0ustar vsevavseva -1 foo 123 return {} luacheck-0.13.0/spec/caches/bad_result.cache0000644000175000017500000000002412642521554017677 0ustar vsevavseva 0 foo 123 return { luacheck-0.13.0/spec/caches/bad_result2.cache0000644000175000017500000000003212642521554017760 0ustar vsevavseva 0 foo 123 return (nil)() luacheck-0.13.0/spec/caches/bad_mtime.cache0000644000175000017500000000002512642521554017475 0ustar vsevavseva 0 foo bar return {} luacheck-0.13.0/spec/caches/bad_lines.cache0000644000175000017500000000001312642521554017471 0ustar vsevavseva 0 foo 123 luacheck-0.13.0/spec/check_spec.lua0000644000175000017500000003316112642521554016142 0ustar vsevavsevalocal check = require "luacheck.check" describe("check", function() it("does not find anything wrong in an empty block", function() assert.same({}, check("")) end) it("does not find anything wrong in used locals", function() assert.same({ {code = "113", name = "print", line = 5, column = 4, end_column = 8} }, check[[ local a local b = 5 a = 6 do print(b, {a}) end ]]) end) it("detects global set", function() assert.same({ {code = "111", name = "foo", line = 1, column = 1, end_column = 3, top = true} }, check[[ foo = {} ]]) end) it("detects global set in nested functions", function() assert.same({ {code = "111", name = "foo", line = 2, column = 4, end_column = 6} }, check[[ local function bar() foo = {} end bar() ]]) end) it("detects global access in multi-assignments", function() assert.same({ {code = "111", name = "y", line = 2, column = 4, end_column = 4, top = true}, {code = "532", line = 2, column = 6, end_column = 6}, {code = "113", name = "print", line = 3, column = 1, end_column = 5} }, check[[ local x x, y = 1 print(x) ]]) end) it("detects global access in self swap", function() assert.same({ {code = "113", name = "a", line = 1, column = 11, end_column = 11}, {code = "113", name = "print", line = 2, column = 1, end_column = 5} }, check[[ local a = a print(a) ]]) end) it("detects global mutation", function() assert.same({ {code = "112", name = "a", line = 1, column = 1, end_column = 1} }, check[[ a[1] = 6 ]]) end) it("detects unused locals", function() assert.same({ {code = "211", name = "a", line = 1, column = 7, end_column = 7}, {code = "113", name = "print", line = 5, column = 4, end_column = 8} }, check[[ local a = 4 do local b = 6 print(b) end ]]) end) it("detects unused locals from function arguments", function() assert.same({ {code = "212", name = "foo", line = 1, column = 17, end_column = 19} }, check[[ return function(foo, ...) return ... end ]]) end) it("detects unused implicit self", function() assert.same({ {code = "212", name = "self", self = true, line = 2, column = 11, end_column = 11} }, check[[ local a = {} function a:b() end ]]) end) it("detects unused locals from loops", function() assert.same({ {code = "213", name = "i", line = 1, column = 5, end_column = 5}, {code = "213", name = "i", line = 2, column = 5, end_column = 5}, {code = "113", name = "pairs", line = 2, column = 10, end_column = 14} }, check[[ for i=1, 2 do end for i in pairs{} do end ]]) end) it("detects unused values", function() assert.same({ {code = "311", name = "a", line = 3, column = 4, end_column = 4}, {code = "311", name = "a", line = 5, column = 4, end_column = 4}, {code = "113", name = "print", line = 9, column = 1, end_column = 5} }, check[[ local a if ... then a = 2 else a = 3 end a = 5 print(a) ]]) end) it("does not detect unused value when it and a closure using it can live together", function() assert.same({ {code = "113", name = "escape", line = 3, column = 4, end_column = 9} }, check[[ local a = 3 if true then escape(function() return a end) end ]]) end) it("does not consider value assigned to upvalue as unused if it is accessed in another closure", function() assert.same({}, check[[ local a local function f(x) a = x end local function g() return a end return f, g ]]) end) it("does not consider a variable initialized if it can't get a value due to short rhs", function() assert.same({}, check[[ local a, b = "foo" b = "bar" return a, b ]]) end) it("considers a variable initialized if short rhs ends with potential multivalue", function() assert.same({ {code = "311", name = "b", line = 2, column = 13, end_column = 13, secondary = true} }, check[[ return function(...) local a, b = ... b = "bar" return a, b end ]]) end) it("reports unused variable as secondary if it is assigned together with a used one", function() assert.same({ {code = "211", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f) local a, b = f() return b end ]]) end) it("reports unused value as secondary if it is assigned together with a used one", function() assert.same({ {code = "231", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f) local a, b a, b = f() return b end ]]) assert.same({ {code = "231", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f, t) local a a, t[1] = f() end ]]) end) it("considers a variable assigned even if it can't get a value due to short rhs (it still gets nil)", function() assert.same({ {code = "311", name = "a", line = 1, column = 7, end_column = 7}, {code = "311", name = "b", line = 1, column = 10, end_column = 10}, {code = "532", line = 2, column = 6, end_column = 6} }, check[[ local a, b = "foo", "bar" a, b = "bar" return a, b ]]) end) it("reports vartype == var when the unused value is not the initial", function() assert.same({ {code = "312", name = "b", line = 1, column = 23, end_column = 23}, {code = "311", name = "a", line = 2, column = 4, end_column = 4} }, check[[ local function foo(a, b) a = a or "default" a = 42 b = 7 return a, b end return foo ]]) end) it("does not detect unused values in loops", function() assert.same({ {code = "113", name = "print", line = 3, column = 4, end_column = 8}, {code = "113", name = "math", line = 4, column = 8, end_column = 11} }, check[[ local a = 10 while a > 0 do print(a) a = math.floor(a/2) end ]]) end) it("handles upvalues before infinite loops", function() assert.same({ {code = "221", name = "x", line = 1, column = 7, end_column = 7}, {code = "211", name = "f", func = true, line = 2, column = 16, end_column = 16} }, check[[ local x local function f() return x end ::loop:: goto loop ]]) end) it("detects redefinition in the same scope", function() assert.same({ {code = "211", name = "foo", line = 1, column = 7, end_column = 9}, {code = "411", name = "foo", line = 2, column = 7, end_column = 9, prev_line = 1, prev_column = 7}, {code = "113", name = "print", line = 3, column = 1, end_column = 5} }, check[[ local foo local foo = "bar" print(foo) ]]) end) it("detects redefinition of function arguments", function() assert.same({ {code = "212", name = "foo", line = 1, column = 17, end_column = 19}, {code = "212", name = "...", line = 1, column = 22, end_column = 24}, {code = "412", name = "foo", line = 2, column = 10, end_column = 12, prev_line = 1, prev_column = 17} }, check[[ return function(foo, ...) local foo = 1 return foo end ]]) end) it("marks redefinition of implicit self", function() assert.same({ {code = "112", name = "t", line = 1, column = 10, end_column = 10}, {code = "212", name = "self", line = 1, column = 11, end_column = 11, self = true}, {code = "212", name = "self", line = 3, column = 14, end_column = 14, self = true}, {code = "432", name = "self", line = 3, column = 14, end_column = 14, self = true, prev_line = 1, prev_column = 11} }, check[[ function t:f() local o = {} function o:g() end end ]]) assert.same({ {code = "112", name = "t", line = 1, column = 10, end_column = 10}, {code = "212", name = "self", line = 1, column = 14, end_column = 17}, {code = "212", name = "self", line = 3, column = 14, end_column = 14, self = true}, {code = "432", name = "self", line = 3, column = 14, end_column = 14, prev_line = 1, prev_column = 14} }, check[[ function t.f(self) local o = {} function o:g() end end ]]) assert.same({ {code = "112", name = "t", line = 1, column = 10, end_column = 10}, {code = "212", name = "self", line = 1, column = 11, end_column = 11, self = true}, {code = "212", name = "self", line = 3, column = 17, end_column = 20}, {code = "432", name = "self", line = 3, column = 17, end_column = 20, prev_line = 1, prev_column = 11} }, check[[ function t:f() local o = {} function o.g(self) end end ]]) end) it("detects shadowing definitions", function() assert.same({ {code = "431", name = "a", line = 4, column = 10, end_column = 10, prev_line = 1, prev_column = 7}, {code = "421", name = "a", line = 7, column = 13, end_column = 13, prev_line = 4, prev_column = 10} }, check[[ local a = 46 return a, function(foo, ...) local a = 1 do local a = 6 foo(a, ...) end return a end ]]) end) it("detects unset variables", function() assert.same({ {code = "221", name = "a", line = 1, column = 7, end_column = 7} }, check[[ local a return a ]]) end) it("detects unused labels", function() assert.same({ {code = "521", name = "fail", line = 2, column = 4, end_column = 11} }, check[[ ::fail:: do ::fail:: end goto fail ]]) end) it("detects unreachable code", function() assert.same({ {code = "511", line = 2, column = 1, end_column = 2} }, check[[ do return end if ... then return 6 end return 3 ]]) assert.same({ {code = "511", line = 7, column = 1, end_column = 2}, {code = "511", line = 13, column = 1, end_column = 6} }, check[[ if ... then return 4 else return 6 end if ... then return 7 else return 8 end return 3 ]]) end) it("detects unreachable code with literal conditions", function() assert.same({ {code = "511", line = 4, column = 1, end_column = 6} }, check[[ while true do (...)() end return ]]) assert.same({}, check[[ repeat if ... then break end until false return ]]) assert.same({ {code = "511", line = 6, column = 1, end_column = 6} }, check[[ repeat if nil then break end until false return ]]) end) it("detects unreachable expressions", function() assert.same({ {code = "511", line = 3, column = 7, end_column = 9} }, check[[ repeat return until ... ]]) assert.same({ {code = "511", line = 3, column = 8, end_column = 10} }, check[[ if true then (...)() elseif ... then (...)() end ]]) end) it("detects unreachable code in nested function", function() assert.same({ {code = "511", line = 4, column = 7, end_column = 12} }, check[[ return function() return function() do return end return end end ]]) end) it("detects accessing uninitialized variables", function() assert.same({ {code = "113", name = "get", line = 6, column = 8, end_column = 10}, {code = "321", name = "a", line = 6, column = 12, end_column = 12} }, check[[ local a if ... then a = 5 else a = get(a) end return a ]]) end) it("detects accessing uninitialized variables in nested functions", function() assert.same({ {code = "113", name = "get", line = 7, column = 8, end_column = 10}, {code = "321", name = "a", line = 7, column = 12, end_column = 12} }, check[[ return function() return function(...) local a if ... then a = 5 else a = get(a) end return a end end ]]) end) it("does not detect accessing unitialized variables incorrectly in loops", function() assert.same({ {code = "113", name = "get", line = 4, column = 8, end_column = 10} }, check[[ local a while not a do a = get() end return a ]]) end) it("detects unbalanced assignments", function() assert.same({ {code = "532", line = 4, column = 6, end_column = 6}, {code = "531", line = 5, column = 6, end_column = 6} }, check[[ local a, b = 4; (...)(a) a, b = (...)(); (...)(a, b) a, b = 5; (...)(a, b) a, b = 1, 2, 3; (...)(a, b) ]]) end) it("detects empty blocks", function() assert.same({ {code = "541", line = 1, column = 1, end_column = 2}, {code = "542", line = 3, column = 8, end_column = 11}, {code = "542", line = 5, column = 12, end_column = 15}, {code = "542", line = 7, column = 1, end_column = 4} }, check[[ do end if ... then elseif ... then else end while ... do end repeat until ... ]]) end) it("detects empty statements", function() assert.same({ {code = "551", line = 1, column = 1, end_column = 1}, {code = "541", line = 2, column = 1, end_column = 2}, {code = "551", line = 2, column = 8, end_column = 8}, {code = "551", line = 4, column = 20, end_column = 20}, {code = "551", line = 7, column = 17, end_column = 17} }, check[[ ; do end;; local foo = "bar"; foo = foo .. "baz";; while true do if foo() then; goto fail; elseif foo() then break; end end ::fail:: return foo; ]]) end) it("handles argparse sample", function() assert.table(check(io.open("spec/samples/argparse.lua", "rb"):read("*a"))) end) end) luacheck-0.13.0/spec/format_spec.lua0000644000175000017500000001020412642521554016346 0ustar vsevavsevalocal format = require "luacheck.format".format local function remove_color(s) return (s:gsub("\27.-\109", "")) end describe("format", function() it("returns formatted report", function() assert.equal([[Checking stdin 1 warning stdin:2:7: unused global variable foo Checking foo.lua 1 warning foo.lua:2:7: empty statement Checking bar.lua OK Checking baz.lua 1 error baz.lua:4:3: something went wrong Total: 2 warnings / 1 error in 4 files]], remove_color(format({ warnings = 2, errors = 1, fatals = 0, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "551", line = 2, column = 7 } }, {}, { { code = "011", line = 4, column = 3, msg = "something went wrong" } } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {}))) end) it("does not output OK messages with options.quiet >= 1", function() assert.equal([[Checking stdin 1 warning stdin:2:7: unused global variable foo Checking foo.lua 1 warning / 1 error foo.lua:2:7: unused global variable foo foo.lua:3:10: invalid inline option Checking baz.lua Syntax error Total: 2 warnings / 1 error in 3 files, couldn't check 1 file]], remove_color(format({ warnings = 2, errors = 1, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 }, { code = "021", line = 3, column = 10 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 1}))) end) it("does not output warnings with options.quiet >= 2", function() assert.equal([[Checking stdin 1 warning Checking foo.lua 1 warning Checking baz.lua Syntax error Total: 2 warnings / 0 errors in 3 files, couldn't check 1 file]], remove_color(format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 2}))) end) it("does not output file info with options.quiet == 3", function() assert.equal("Total: 2 warnings / 0 errors in 3 files, couldn't check 1 file", remove_color(format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 3}))) end) it("does not color output if options.color == false", function() assert.equal([[Checking stdin 1 warning stdin:2:7: unused global variable 'foo' Checking foo.lua 1 warning foo.lua:2:7: unused global variable 'foo' Checking bar.lua OK Checking baz.lua Syntax error Total: 2 warnings / 0 errors in 3 files, couldn't check 1 file]], format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {color = false})) end) end) luacheck-0.13.0/spec/options_spec.lua0000644000175000017500000001144012642521554016554 0ustar vsevavsevalocal options = require "luacheck.options" describe("options", function() describe("validate", function() it("returns true if options are empty", function() assert.is_true(options.validate(options.all_options)) end) it("returns true if options are valid", function() assert.is_true(options.validate(options.all_options, { globals = {"foo"}, compat = false, unrelated = function() end })) end) it("returns false if options are invalid", function() assert.is_false(options.validate(options.all_options, { globals = 3, redefined = false })) assert.is_false(options.validate(options.all_options, { globals = {3} })) assert.is_false(options.validate(options.all_options, function() end)) assert.is_false(options.validate(options.all_options, { unused = 0 })) end) it("additionally returns name of the problematic field", function() assert.equal("globals", select(2, options.validate(options.all_options, { globals = 3, redefined = false }))) assert.equal("globals", select(2, options.validate(options.all_options, { globals = {3} }))) assert.equal("unused", select(2, options.validate(options.all_options, { unused = 0 }))) end) end) describe("normalize", function() it("applies default values", function() local opts = options.normalize({}) assert.same(opts, options.normalize({{}})) assert.is_true(opts.unused_secondaries) assert.is_false(opts.module) assert.is_false(opts.allow_defined) assert.is_false(opts.allow_defined_top) assert.is_table(opts.globals) assert.same({}, opts.rules) end) it("considers simple boolean options", function() local opts = options.normalize({ { module = false, unused_secondaries = true }, { module = true, allow_defined = false } }) assert.is_true(opts.module) assert.is_true(opts.unused_secondaries) assert.is_false(opts.allow_defined) end) it("considers opts.std and opts.compat", function() assert.same({baz = 1}, options.normalize({ { std = "none" }, { globals = {"foo", "bar"}, compat = true }, { new_globals = {"baz"}, compat = false } }).globals) end) it("allows compound std unions", function() assert.same(options.normalize({ { std = "max" }, }).globals, options.normalize({ { std = "lua51+lua52+lua53+luajit" }, }).globals) end) it("allows std addition", function() assert.same(options.normalize({ { std = "lua52 + busted" }, }).globals, options.normalize({ { std = "max" }, { std = "none" }, { std = "+lua52+busted" } }).globals) end) it("considers read-only and regular globals", function() local opts = options.normalize({ { std = "lua52", globals = {"foo", "bar"}, read_globals = {"baz"} }, { new_read_globals = {"quux"}, } }) local globals = opts.globals local read_globals = opts.read_globals assert.is_truthy(globals.foo) assert.is_truthy(globals.bar) assert.is_nil(globals.baz) assert.is_truthy(globals.quux) assert.is_truthy(read_globals.quux) assert.is_truthy(read_globals.string) assert.is_nil(read_globals._ENV) assert.is_truthy(globals.string) assert.is_truthy(globals._ENV) end) it("considers macros, ignore, enable and only", function() assert.same({ {{{nil, "^foo$"}}, "only"}, {{{"^21[23]", nil}}, "disable"}, {{{"^[23]", nil}}, "enable"}, {{{"^511", nil}}, "enable"}, {{{"^412", nil}, {"1$", "^bar$"}}, "disable"} }, options.normalize({ { unused = false }, { ignore = {"412", "1$/bar"} }, { unused = true, unused_args = false, enable = {"511"} }, { only = {"foo"} } }).rules) end) end) end) luacheck-0.13.0/spec/globbing_spec.lua0000644000175000017500000000567012642521554016654 0ustar vsevavsevalocal globbing = require "luacheck.globbing" describe("globbing", function() describe("match", function() it("returns true on literal match", function() assert.is_true(globbing.match("foo/bar", "foo/bar")) end) it("returns true on literal match after normalization", function() assert.is_true(globbing.match("foo//bar/baz/..", "./foo/bar/")) end) it("returns false for on literal mismatch", function() assert.is_false(globbing.match("foo/bar", "foo/baz")) end) it("accepts subdirectory matches", function() assert.is_true(globbing.match("foo/bar", "foo/bar/baz")) end) it("understands wildcards", function() assert.is_true(globbing.match("*", "foo")) assert.is_true(globbing.match("foo/*r", "foo/bar")) assert.is_true(globbing.match("foo/*r", "foo/bar/baz")) assert.is_false(globbing.match("foo/*r", "foo/baz")) end) it("understands optional characters", function() assert.is_false(globbing.match("?", "foo")) assert.is_true(globbing.match("???", "foo")) assert.is_true(globbing.match("????", "foo")) assert.is_true(globbing.match("f?o/?a?", "foo/bar")) assert.is_false(globbing.match("f?o/?a?", "foo/abc")) end) it("understands ranges and classes", function() assert.is_true(globbing.match("[d-h]o[something]", "foo")) assert.is_false(globbing.match("[d-h]o[somewhere]", "bar")) assert.is_false(globbing.match("[.-h]o[i-z]", "bar")) end) it("accepts closing bracket as first class character", function() assert.is_true(globbing.match("[]]", "]")) assert.is_false(globbing.match("[]]", "[")) assert.is_true(globbing.match("[]foo][]foo][]foo]", "foo")) end) it("accepts dash as first or last class character", function() assert.is_true(globbing.match("[-]", "-")) assert.is_false(globbing.match("[-]", "+")) assert.is_true(globbing.match("[---]", "-")) end) it("understands negation", function() assert.is_true(globbing.match("[!foo][!bar][!baz]", "boo")) assert.is_false(globbing.match("[!foo][!bar][!baz]", "far")) assert.is_false(globbing.match("[!a-z]", "g")) end) it("understands recursive globbing using **", function() assert.is_true(globbing.match("**/*.lua", "foo.lua")) assert.is_true(globbing.match("**/*.lua", "foo/bar.lua")) assert.is_false(globbing.match("foo/**/*.lua", "bar.lua")) assert.is_false(globbing.match("foo/**/*.lua", "foo.lua")) assert.is_true(globbing.match("foo/**/bar/*.lua", "foo/bar/baz.lua")) assert.is_true(globbing.match("foo/**/bar/*.lua", "foo/foo2/foo3/bar/baz.lua")) assert.is_false(globbing.match("foo/**/bar/*.lua", "foo/baz.lua")) assert.is_false(globbing.match("foo/**/bar/*.lua", "bar/baz.lua")) end) end) end) luacheck-0.13.0/spec/config_spec.lua0000644000175000017500000001242112642521554016326 0ustar vsevavsevalocal config = require "luacheck.config" local fs = require "luacheck.fs" local cur_dir = fs.has_lfs and fs.lfs.currentdir() local function nest(dir, func) if fs.has_lfs then local backed = false local function back() if not backed then fs.lfs.chdir(cur_dir) backed = true end end finally(back) fs.lfs.chdir(dir) func() back() end end describe("config", function() it("has default path", function() assert.is_string(config.default_path) end) it("loads default config", function() local conf = config.load_config() assert.is_table(conf) nest("spec/configs", function() local nested_conf = config.load_config() assert.is_table(nested_conf) assert.same(config.get_top_options(conf), config.get_top_options(nested_conf)) assert.same(config.get_options(conf, "spec/foo.lua"), config.get_options(nested_conf, "../foo.lua")) assert.equal("../../bar.lua", config.relative_path(nested_conf, "bar.lua")) end) assert.not_same(config.get_options(conf, "spec/foo.lua"), config.get_options(conf, "foo.lua")) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("works with empty config", function() local conf = config.empty_config assert.is_table(conf) assert.same({}, config.get_top_options(conf)) assert.same({}, config.get_options(conf, "bar.lua")) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("loads config from path", function() local conf = config.load_config("spec/configs/override_config.luacheckrc") assert.is_table(conf) nest("spec/configs/project", function() local nested_conf = config.load_config("spec/configs/override_config.luacheckrc") assert.is_table(nested_conf) assert.same(config.get_top_options(conf), config.get_top_options(nested_conf)) assert.same(config.get_options(conf, "spec/samples/bad_code.lua"), config.get_options(nested_conf, "../../samples/bad_code.lua")) assert.equal("../../../bar.lua", config.relative_path(nested_conf, "bar.lua")) end) assert.not_same(config.get_options(conf, "spec/samples/bad_code.lua"), config.get_options(conf, "spec/samples/unused_code.lua")) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("returns nil, error on missing config", function() local conf, err = config.load_config("spec/configs/config_404.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't find configuration file spec/configs/config_404.luacheckrc", err) end) it("returns nil, error on config with bad syntax", function() local conf, err = config.load_config("spec/configs/bad_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from spec/configs/bad_config.luacheckrc: syntax error", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config("spec/configs/bad_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from ../../../spec/configs/bad_config.luacheckrc: syntax error", nested_err) end) end) it("returns nil, error on config with runtime issues", function() local conf, err = config.load_config("spec/configs/runtime_bad_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from spec/configs/runtime_bad_config.luacheckrc: runtime error", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config("spec/configs/runtime_bad_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from ../../../spec/configs/runtime_bad_config.luacheckrc: runtime error", nested_err) end) end) it("returns nil, error on invalid config", function() local conf, err = config.load_config("spec/configs/invalid_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from spec/configs/invalid_config.luacheckrc: invalid value of option 'ignore'", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config("spec/configs/invalid_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from ../../../spec/configs/invalid_config.luacheckrc: invalid value of option 'ignore'", nested_err) end) end) it("returns nil, error on config with invalid override", function() local conf, err = config.load_config("spec/configs/invalid_override_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from spec/configs/invalid_override_config.luacheckrc: invalid value of option 'enable' in options for path 'spec/foo.lua'", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config("spec/configs/invalid_override_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from ../../../spec/configs/invalid_override_config.luacheckrc: invalid value of option 'enable' in options for path 'spec/foo.lua'", nested_err) end) end) end) luacheck-0.13.0/spec/cli_spec.lua0000644000175000017500000014621112642521554015635 0ustar vsevavsevalocal utils = require "luacheck.utils" local multithreading = require "luacheck.multithreading" local helper = require "spec.helper" local luacheck_cmd = helper.luacheck_command() setup(helper.before_command) teardown(helper.after_command) local function get_output(command, wd, color) command = ("%s %s 2>&1"):format(helper.luacheck_command(wd), command) local handler = io.popen(command) local output = handler:read("*a"):gsub("\27.-\109", color and "#" or "") handler:close() return output end local function get_exitcode(command) local code51, _, code52 = os.execute(luacheck_cmd.." "..command.." > /dev/null 2>&1") return _VERSION:find "5.1" and code51/256 or code52 end describe("cli", function() it("exists", function() assert.equal(0, get_exitcode "--help") end) it("works for correct files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua --no-config") assert.equal(0, get_exitcode "spec/samples/good_code.lua --no-config") end) it("removes ./ in the beginnings of file names", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "./spec/samples/good_code.lua --no-config") end) it("allows setting new filename", function() assert.equal([[ Checking new.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua --no-config --filename new.lua") end) it("filters files using --exclude-files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua --no-config --exclude-files '**/??d_code.lua'") end) it("filters files using --include-files", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua --no-config --include-files '**/??d_code.lua' -qq") end) it("--exclude-files has priority over --include-files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua --no-config --include-files '**/*.lua' --exclude-files '**/??d_code.lua'") end) it("works for incorrect files", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --no-config") assert.equal(1, get_exitcode "spec/samples/bad_code.lua --no-config") end) it("works for incorrect patterns in options", function() assert.equal([[ Critical error: Invalid pattern '^%1foo$' ]], get_output "spec/samples/bad_code.lua --ignore %1foo --no-config") end) it("checks stdin when given -", function() assert.equal([[ Checking stdin 5 warnings stdin:3:16: unused function helper stdin:3:23: unused variable length argument stdin:7:10: setting non-standard global variable embrace stdin:8:10: variable opt was previously defined as an argument on line 7 stdin:9:11: accessing undefined variable hepler Total: 5 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/override_config.luacheckrc < spec/samples/bad_code.lua") end) it("colors output", function() assert.equal([[ Checking spec/samples/good_code.lua ###OK# Checking spec/samples/bad_code.lua ###5 warnings# spec/samples/bad_code.lua:3:16: unused function ##helper# spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable ##embrace# spec/samples/bad_code.lua:8:10: variable ##opt# was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable ##hepler# Total: ###5# warnings / ###0# errors in 2 files ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-config", nil, true)) end) it("does not color output with --no-color", function() assert.equal([[ Checking spec/samples/good_code.lua OK Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 5 warnings / 0 errors in 2 files ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-color --no-config", nil, true)) end) it("suppresses OK output with -q", function() assert.equal([[Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument baz spec/samples/unused_code.lua:4:8: unused loop variable i spec/samples/unused_code.lua:5:13: unused variable q spec/samples/unused_code.lua:7:11: unused loop variable a spec/samples/unused_code.lua:7:14: unused loop variable b spec/samples/unused_code.lua:7:17: unused loop variable c spec/samples/unused_code.lua:13:7: value assigned to variable x is unused spec/samples/unused_code.lua:14:1: value assigned to variable x is unused spec/samples/unused_code.lua:21:7: variable z is never accessed Total: 14 warnings / 0 errors in 3 files ]], get_output "-q spec/samples/*d_code.lua --no-config") assert.equal([[ Total: 0 warnings / 0 errors in 1 file ]], get_output "-q spec/samples/good_code.lua --no-config") end) it("suppresses warnings output with -qq", function() assert.equal([[Checking spec/samples/bad_code.lua 5 warnings Checking spec/samples/unused_code.lua 9 warnings Total: 14 warnings / 0 errors in 3 files ]], get_output "-qq spec/samples/*d_code.lua --no-config") end) it("suppresses file info output with -qqq", function() assert.equal([[Total: 14 warnings / 0 errors in 3 files ]], get_output "-qqq spec/samples/*d_code.lua --no-config") end) it("allows to ignore some types of warnings", function() assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 3 warnings / 0 errors in 1 file ]], get_output "-u spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 Total: 3 warnings / 0 errors in 1 file ]], get_output "-g spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 4 warnings / 0 errors in 1 file ]], get_output "-r spec/samples/bad_code.lua --no-config") end) it("allows to define additional globals", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --globals embrace --no-config") end) it("allows to set standard globals", function() assert.equal([[ Checking spec/samples/bad_code.lua 6 warnings spec/samples/bad_code.lua:1:1: accessing undefined variable package spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 6 warnings / 0 errors in 1 file ]], get_output "--std none spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 5 warnings / 0 errors in 1 file ]], get_output "--std lua51+lua52+lua53 spec/samples/bad_code.lua --no-config") end) it("allows to ignore some variables", function() assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --ignore embrace opt --no-config") end) it("allows to only watch some variables", function() assert.equal([[ Checking spec/samples/bad_code.lua 1 warning spec/samples/bad_code.lua:3:16: unused function helper Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --only helper --no-config") end) it("recognizes different types of variables", function() assert.equal([[ Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument baz spec/samples/unused_code.lua:4:8: unused loop variable i spec/samples/unused_code.lua:5:13: unused variable q spec/samples/unused_code.lua:7:11: unused loop variable a spec/samples/unused_code.lua:7:14: unused loop variable b spec/samples/unused_code.lua:7:17: unused loop variable c spec/samples/unused_code.lua:13:7: value assigned to variable x is unused spec/samples/unused_code.lua:14:1: value assigned to variable x is unused spec/samples/unused_code.lua:21:7: variable z is never accessed Total: 9 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_code.lua --no-config") end) it("allows to ignore unused arguments", function() assert.equal([[ Checking spec/samples/unused_code.lua 4 warnings spec/samples/unused_code.lua:5:13: unused variable q spec/samples/unused_code.lua:13:7: value assigned to variable x is unused spec/samples/unused_code.lua:14:1: value assigned to variable x is unused spec/samples/unused_code.lua:21:7: variable z is never accessed Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_code.lua --no-unused-args --no-config") end) it("allows to ignore unused secondary values and variables", function() assert.equal([[ Checking spec/samples/unused_secondaries.lua 4 warnings spec/samples/unused_secondaries.lua:3:7: unused variable a spec/samples/unused_secondaries.lua:6:7: unused variable x spec/samples/unused_secondaries.lua:6:13: unused variable z spec/samples/unused_secondaries.lua:12:1: value assigned to variable o is unused Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_secondaries.lua --no-config") assert.equal([[ Checking spec/samples/unused_secondaries.lua 1 warning spec/samples/unused_secondaries.lua:6:7: unused variable x Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/unused_secondaries.lua -s --no-config") end) it("allows to ignore warnings related to implicit self", function() assert.equal([[ Checking spec/samples/redefined.lua 5 warnings spec/samples/redefined.lua:4:10: shadowing upvalue a on line 1 spec/samples/redefined.lua:4:13: variable self is never set spec/samples/redefined.lua:4:13: variable self was previously defined as an argument on line 3 spec/samples/redefined.lua:7:13: shadowing definition of variable a on line 4 spec/samples/redefined.lua:8:32: shadowing upvalue self on line 4 Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/redefined.lua --no-self --globals each --no-config") end) it("handles errors gracefully", function() assert.equal([[ Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Checking spec/samples/absent_code.lua I/O error Total: 0 warnings / 1 error in 1 file, couldn't check 1 file ]], get_output "spec/samples/python_code.lua spec/samples/absent_code.lua --no-config") assert.equal(2, get_exitcode "spec/samples/python_code.lua spec/samples/absent_code.lua --no-config") end) it("expands rockspecs", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/good_code.lua OK Total: 5 warnings / 0 errors in 2 files ]], get_output "spec/samples/sample.rockspec --no-config") end) it("handles bad rockspecs", function() assert.equal([[ Checking spec/samples/bad.rockspec Syntax error Total: 0 warnings / 0 errors in 0 files, couldn't check 1 file ]], get_output "spec/samples/bad.rockspec --no-config") end) it("allows ignoring defined globals", function() assert.equal([[ Checking spec/samples/defined.lua 1 warning spec/samples/defined.lua:4:4: accessing undefined variable baz Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined.lua spec/samples/defined2.lua -d --no-config") assert.equal([[ Checking spec/samples/defined2.lua OK Checking spec/samples/defined.lua 1 warning spec/samples/defined.lua:4:4: accessing undefined variable baz Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined2.lua spec/samples/defined.lua -d --no-config") end) it("allows restricting scope of defined globals to the file with their definition", function() assert.equal([[ Checking spec/samples/defined2.lua 1 warning spec/samples/defined2.lua:1:1: accessing undefined variable foo Checking spec/samples/defined3.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined2.lua spec/samples/defined3.lua -d -m --no-config") end) it("allows ignoring globals defined in top level scope", function() assert.equal([[ Checking spec/samples/defined4.lua 2 warnings spec/samples/defined4.lua:1:10: unused global variable foo spec/samples/defined4.lua:3:4: setting non-standard global variable bar Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined4.lua -t --no-config") end) it("detects unused defined globals", function() assert.equal([[ Checking spec/samples/defined3.lua 3 warnings spec/samples/defined3.lua:1:1: unused global variable foo spec/samples/defined3.lua:2:1: unused global variable foo spec/samples/defined3.lua:3:1: unused global variable bar Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -d --no-config") assert.equal([[ Checking spec/samples/defined3.lua 1 warning spec/samples/defined3.lua:3:1: unused global variable bar Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -d --no-config") end) it("treats `unused global` warnings as `global` type warnings", function() assert.equal([[ Checking spec/samples/defined3.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -gd --no-config") assert.equal([[ Checking spec/samples/defined3.lua 1 warning spec/samples/defined3.lua:3:1: unused global variable bar Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -ud --no-config") end) it("allows ignoring unused defined globals", function() assert.equal([[ Checking spec/samples/defined3.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -d --ignore 13 --no-config") assert.equal([[ Checking spec/samples/defined3.lua OK Checking spec/samples/defined2.lua OK Total: 0 warnings / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -d --ignore 13 --no-config") end) it("detects flow issues", function() assert.equal([[ Checking spec/samples/bad_flow.lua 6 warnings spec/samples/bad_flow.lua:1:28: empty if branch spec/samples/bad_flow.lua:6:4: empty do..end block spec/samples/bad_flow.lua:12:15: left-hand side of assignment is too long spec/samples/bad_flow.lua:16:15: left-hand side of assignment is too short spec/samples/bad_flow.lua:21:7: unreachable code spec/samples/bad_flow.lua:25:1: loop is executed at most once Total: 6 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_flow.lua --no-config") end) it("detects redefinitions", function() assert.equal([[ Checking spec/samples/redefined.lua 6 warnings spec/samples/redefined.lua:3:11: unused argument self spec/samples/redefined.lua:4:10: shadowing upvalue a on line 1 spec/samples/redefined.lua:4:13: variable self is never set spec/samples/redefined.lua:4:13: variable self was previously defined as an argument on line 3 spec/samples/redefined.lua:7:13: shadowing definition of variable a on line 4 spec/samples/redefined.lua:8:32: shadowing upvalue self on line 4 Total: 6 warnings / 0 errors in 1 file ]], get_output "spec/samples/redefined.lua --globals each --no-config") end) it("detects issues related to read-only globals", function() assert.equal([[ Checking spec/samples/read_globals.lua 5 warnings spec/samples/read_globals.lua:1:1: setting read-only global variable string spec/samples/read_globals.lua:2:1: mutating read-only global variable table spec/samples/read_globals.lua:5:1: setting read-only global variable bar spec/samples/read_globals.lua:6:1: mutating non-standard global variable baz spec/samples/read_globals.lua:6:21: accessing undefined variable baz Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals.lua --std=lua52 --globals foo --read-globals bar --no-config") end) it("allows showing warning codes", function() assert.equal([[ Checking spec/samples/read_globals.lua 5 warnings spec/samples/read_globals.lua:1:1: (W121) setting read-only global variable string spec/samples/read_globals.lua:2:1: (W122) mutating read-only global variable table spec/samples/read_globals.lua:5:1: (W121) setting read-only global variable bar spec/samples/read_globals.lua:6:1: (W112) mutating non-standard global variable baz spec/samples/read_globals.lua:6:21: (W113) accessing undefined variable baz Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals.lua --std=lua52 --globals foo --read-globals bar --codes --no-config") end) it("allows showing token ranges", function() assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors spec/samples/inline_options.lua:6:16-16: unused function f spec/samples/inline_options.lua:12:4-5: accessing undefined variable qu spec/samples/inline_options.lua:15:1-3: accessing undefined variable baz spec/samples/inline_options.lua:22:10-10: unused variable g spec/samples/inline_options.lua:24:7-7: unused variable f spec/samples/inline_options.lua:24:10-10: unused variable g spec/samples/inline_options.lua:26:1-17: unpaired push directive spec/samples/inline_options.lua:28:4-19: unpaired pop directive spec/samples/inline_options.lua:34:1-2: empty do..end block spec/samples/inline_options.lua:35:10-13: empty if branch Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6-15: expected '=' near '__future__' Total: 8 warnings / 3 errors in 2 files ]], get_output "spec/samples/inline_options.lua spec/samples/python_code.lua --ranges --no-config") end) it("applies inline options", function() assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors spec/samples/inline_options.lua:6:16: unused function f spec/samples/inline_options.lua:12:4: accessing undefined variable qu spec/samples/inline_options.lua:15:1: accessing undefined variable baz spec/samples/inline_options.lua:22:10: unused variable g spec/samples/inline_options.lua:24:7: unused variable f spec/samples/inline_options.lua:24:10: unused variable g spec/samples/inline_options.lua:26:1: unpaired push directive spec/samples/inline_options.lua:28:4: unpaired pop directive spec/samples/inline_options.lua:34:1: empty do..end block spec/samples/inline_options.lua:35:10: empty if branch Total: 8 warnings / 2 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --no-config") assert.equal([[ Checking spec/samples/inline_options.lua 7 warnings / 2 errors spec/samples/inline_options.lua:6:16: unused function f spec/samples/inline_options.lua:12:4: accessing undefined variable qu spec/samples/inline_options.lua:15:1: accessing undefined variable baz spec/samples/inline_options.lua:22:10: unused variable g spec/samples/inline_options.lua:24:7: unused variable f spec/samples/inline_options.lua:24:10: unused variable g spec/samples/inline_options.lua:26:1: unpaired push directive spec/samples/inline_options.lua:28:4: unpaired pop directive spec/samples/inline_options.lua:34:1: empty do..end block Total: 7 warnings / 2 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --ignore=542 --no-config") assert.equal([[ Checking spec/samples/global_inline_options.lua 3 warnings spec/samples/global_inline_options.lua:6:10: unused global variable f spec/samples/global_inline_options.lua:7:4: setting non-standard global variable baz spec/samples/global_inline_options.lua:18:4: setting non-module global variable external Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/global_inline_options.lua --std=lua52 --no-config") assert.equal([[ Checking spec/samples/read_globals_inline_options.lua 5 warnings spec/samples/read_globals_inline_options.lua:2:10: accessing undefined variable baz spec/samples/read_globals_inline_options.lua:3:1: setting read-only global variable foo spec/samples/read_globals_inline_options.lua:3:11: setting non-standard global variable baz spec/samples/read_globals_inline_options.lua:3:16: mutating non-standard global variable baz spec/samples/read_globals_inline_options.lua:5:1: setting read-only global variable foo Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals_inline_options.lua --std=lua52 --no-config") assert.equal([[ Checking spec/samples/read_globals_inline_options.lua 1 warning spec/samples/read_globals_inline_options.lua:3:16: mutating read-only global variable baz Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/read_globals_inline_options.lua --std=lua52 --read-globals baz --globals foo --no-config") end) it("inline options can use extended stds", function() assert.equal([[ Checking spec/samples/custom_std_inline_options.lua 2 warnings spec/samples/custom_std_inline_options.lua:3:1: accessing undefined variable tostring spec/samples/custom_std_inline_options.lua:6:25: accessing undefined variable it Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/custom_std_inline_options.lua --config=spec/configs/custom_stds_config.luacheckrc") end) it("inline options can be disabled", function() assert.equal([[ Checking spec/samples/inline_options.lua 26 warnings spec/samples/inline_options.lua:3:1: accessing undefined variable foo spec/samples/inline_options.lua:4:1: accessing undefined variable bar spec/samples/inline_options.lua:6:16: unused function f spec/samples/inline_options.lua:6:18: unused argument a spec/samples/inline_options.lua:8:4: accessing undefined variable foo spec/samples/inline_options.lua:9:4: accessing undefined variable bar spec/samples/inline_options.lua:10:4: accessing undefined variable baz spec/samples/inline_options.lua:11:4: accessing undefined variable qu spec/samples/inline_options.lua:12:4: accessing undefined variable qu spec/samples/inline_options.lua:15:1: accessing undefined variable baz spec/samples/inline_options.lua:18:7: unused variable f spec/samples/inline_options.lua:18:7: variable f was previously defined on line 6 spec/samples/inline_options.lua:20:7: unused variable g spec/samples/inline_options.lua:22:7: unused variable f spec/samples/inline_options.lua:22:7: variable f was previously defined on line 18 spec/samples/inline_options.lua:22:10: unused variable g spec/samples/inline_options.lua:22:10: variable g was previously defined on line 20 spec/samples/inline_options.lua:24:7: unused variable f spec/samples/inline_options.lua:24:7: variable f was previously defined on line 22 spec/samples/inline_options.lua:24:10: unused variable g spec/samples/inline_options.lua:24:10: variable g was previously defined on line 22 spec/samples/inline_options.lua:27:16: unused function f spec/samples/inline_options.lua:27:16: variable f was previously defined on line 24 spec/samples/inline_options.lua:32:1: empty do..end block spec/samples/inline_options.lua:34:1: empty do..end block spec/samples/inline_options.lua:35:10: empty if branch Total: 26 warnings / 0 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --no-inline --no-config") end) describe("caching", function() local tmpname before_each(function() tmpname = os.tmpname() end) after_each(function() os.remove(tmpname) end) it("caches results", function() local normal_output = [[ Checking spec/samples/good_code.lua OK Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Total: 5 warnings / 1 error in 3 files ]] local mocked_output = [[ Checking spec/samples/good_code.lua 1 error spec/samples/good_code.lua:5:7: this code is actually bad Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/python_code.lua 2 warnings spec/samples/python_code.lua:1:1: setting non-standard global variable global spec/samples/python_code.lua:6:8: accessing uninitialized variable uninit Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument baz spec/samples/unused_code.lua:4:8: unused loop variable i spec/samples/unused_code.lua:5:13: unused variable q spec/samples/unused_code.lua:7:11: unused loop variable a spec/samples/unused_code.lua:7:14: unused loop variable b spec/samples/unused_code.lua:7:17: unused loop variable c spec/samples/unused_code.lua:13:7: value assigned to variable x is unused spec/samples/unused_code.lua:14:1: value assigned to variable x is unused spec/samples/unused_code.lua:21:7: variable z is never accessed Total: 16 warnings / 1 error in 4 files ]] assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) local cache = utils.read_file(tmpname) assert.string(cache) local format_version, good_mtime, bad_mtime, python_mtime = cache:match([[ (%d+) spec/samples/good_code.lua (%d+) return {} spec/samples/bad_code.lua (%d+) local A="113";return {{A,"package",1,1,7},{"211","helper",3,16,21,%[10%]=true},{"212","...",3,23,25},{"111","embrace",7,10,16,%[12%]=true},{"412","opt",8,10,12,7,18},{A,"hepler",9,11,16}} spec/samples/python_code.lua (%d+) return {{"011",%[3%]=1,%[4%]=6,%[5%]=15,%[23%]="expected '=' near '__future__'"}} ]]) format_version = tonumber(format_version) assert.number(format_version) assert.string(good_mtime) assert.string(bad_mtime) assert.string(python_mtime) assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) local function write_new_cache(version) local fh = io.open(tmpname, "w") assert.userdata(fh) fh:write(([[ %s spec/samples/python_code.lua %s return {{"111", "global", 1, 1}, {"321", "uninit", 6, 8}} spec/samples/good_code.lua %s return {{"011",[3]=5,[4]=7,[23]="this code is actually bad"}} spec/samples/bad_code.lua %s return {}]]):format(version, python_mtime, good_mtime, tostring(tonumber(bad_mtime) - 1))) fh:close() end write_new_cache("\n"..tostring(format_version)) assert.equal(mocked_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua spec/samples/unused_code.lua --std=lua52 --no-config --cache "..tmpname)) assert.equal(mocked_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua spec/samples/unused_code.lua --std=lua52 --no-config --cache "..tmpname)) write_new_cache("\n"..tostring(format_version + 1)) assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) write_new_cache("") assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) end) end) if not multithreading.has_lanes then pending("uses multithreading") else it("uses multithreading", function() assert.equal([[ Checking spec/samples/good_code.lua OK Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Total: 5 warnings / 1 error in 3 files ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 -j2 --no-config") end) end it("allows using custom formatter", function() assert.equal([[Files: 2 Formatter: spec.formatters.custom_formatter Quiet: 1 Color: false Codes: true ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua --formatter spec.formatters.custom_formatter -q --codes --no-color --no-config") end) it("loads custom formatters relatively to project root", function() assert.equal([[Files: 2 Formatter: spec.formatters.custom_formatter Quiet: 1 Color: false Codes: true ]], get_output("samples/good_code.lua samples/bad_code.lua --formatter spec.formatters.custom_formatter -q --codes --no-color --no-config", "spec/")) end) it("has built-in TAP formatter", function() assert.equal([[ 1..7 ok 1 spec/samples/good_code.lua not ok 2 spec/samples/bad_code.lua:3:16: unused function 'helper' not ok 3 spec/samples/bad_code.lua:3:23: unused variable length argument not ok 4 spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' not ok 5 spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 not ok 6 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' not ok 7 spec/samples/python_code.lua:1:6: expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter TAP --no-config") assert.equal([[ 1..7 ok 1 spec/samples/good_code.lua not ok 2 spec/samples/bad_code.lua:3:16: (W211) unused function 'helper' not ok 3 spec/samples/bad_code.lua:3:23: (W212) unused variable length argument not ok 4 spec/samples/bad_code.lua:7:10: (W111) setting non-standard global variable 'embrace' not ok 5 spec/samples/bad_code.lua:8:10: (W412) variable 'opt' was previously defined as an argument on line 7 not ok 6 spec/samples/bad_code.lua:9:11: (W113) accessing undefined variable 'hepler' not ok 7 spec/samples/python_code.lua:1:6: (E011) expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter TAP --codes --no-config") end) it("has built-in JUnit formatter", function() assert.equal([[ ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter JUnit --no-config") end) it("has built-in simple warning-per-line formatter", function() assert.equal("", get_output "spec/samples/good_code.lua --std=lua52 --formatter plain --no-config") assert.equal([[ spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' spec/samples/python_code.lua:1:6: expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter plain --no-config") assert.equal([[ spec/samples/404.lua: I/O error ]], get_output "spec/samples/404.lua --formatter plain --no-config") assert.equal([[ spec/samples/bad_code.lua:3:16: (W211) unused function 'helper' spec/samples/bad_code.lua:3:23: (W212) unused variable length argument spec/samples/bad_code.lua:7:10: (W111) setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: (W412) variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: (W113) accessing undefined variable 'hepler' spec/samples/python_code.lua:1:6: (E011) expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter plain --codes --no-config") end) it("provides version info", function() local output = get_output "--version" assert.truthy(output:match("^Luacheck: [%w%p ]+\nLua: [%w%p ]+\nLuaFileSystem: [%w%p ]+\nLuaLanes: [%w%p ]+\n$")) end) it("expands folders", function() assert.matches("^Total: %d+ warnings / %d+ errors in 20 files\n$", get_output "spec/samples -qqq --no-config") end) it("uses --include-files when expanding folders", function() assert.matches("^Total: %d+ warnings / %d+ errors in 2 files\n$", get_output "spec/samples -qqq --no-config --include-files '**/*.rockspec'") end) describe("config", function() describe("loading", function() it("uses .luacheckrc in current directory if possible", function() assert.equal([[ Checking nested/ab.lua 1 warning nested/ab.lua:1:10: accessing undefined variable b Checking nested/nested/abc.lua 2 warnings nested/nested/abc.lua:1:7: accessing undefined variable a nested/nested/abc.lua:1:13: accessing undefined variable c Total: 3 warnings / 0 errors in 2 files ]], get_output("nested", "spec/configs/project/")) end) it("does not use .luacheckrc in current directory with --no-config", function() assert.equal([[ Checking nested/ab.lua 2 warnings nested/ab.lua:1:7: accessing undefined variable a nested/ab.lua:1:10: accessing undefined variable b Checking nested/nested/abc.lua 3 warnings nested/nested/abc.lua:1:7: accessing undefined variable a nested/nested/abc.lua:1:10: accessing undefined variable b nested/nested/abc.lua:1:13: accessing undefined variable c Total: 5 warnings / 0 errors in 2 files ]], get_output("nested --no-config", "spec/configs/project/")) end) it("uses .luacheckrc in upper directory", function() assert.equal([[ Checking ab.lua 1 warning ab.lua:1:10: accessing undefined variable b Checking nested/abc.lua 2 warnings nested/abc.lua:1:7: accessing undefined variable a nested/abc.lua:1:13: accessing undefined variable c Total: 3 warnings / 0 errors in 2 files ]], get_output("ab.lua nested", "spec/configs/project/nested/")) end) it("uses config provided with --config=path", function() assert.equal([[ Checking spec/samples/compat.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --config=spec/configs/global_config.luacheckrc") end) it("uses config when checking stdin", function() assert.equal([[ Checking stdin OK Total: 0 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/global_config.luacheckrc < spec/samples/compat.lua") end) it("uses per-file overrides", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/unused_code.lua OK Total: 4 warnings / 0 errors in 2 files ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc") end) it("uses new filename when selecting per-file overrides", function() assert.equal([[ Checking spec/samples/unused_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/override_config.luacheckrc --filename spec/samples/unused_code.lua < spec/samples/unused_code.lua") end) it("uses all overrides prefixing file name", function() assert.equal([[ Checking spec/samples/unused_secondaries.lua 1 warning spec/samples/unused_secondaries.lua:12:1: value assigned to variable o is unused Checking spec/samples/unused_code.lua 7 warnings spec/samples/unused_code.lua:3:18: unused argument baz spec/samples/unused_code.lua:4:8: unused loop variable i spec/samples/unused_code.lua:7:11: unused loop variable a spec/samples/unused_code.lua:7:14: unused loop variable b spec/samples/unused_code.lua:7:17: unused loop variable c spec/samples/unused_code.lua:13:7: value assigned to variable x is unused spec/samples/unused_code.lua:14:1: value assigned to variable x is unused Total: 8 warnings / 0 errors in 2 files ]], get_output "spec/samples/unused_secondaries.lua spec/samples/unused_code.lua --config=spec/configs/multioverride_config.luacheckrc") end) it("allows reenabling warnings ignored in config using --enable", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function helper spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable embrace spec/samples/bad_code.lua:9:11: accessing undefined variable hepler Checking spec/samples/unused_code.lua 1 warning spec/samples/unused_code.lua:5:13: unused variable q Total: 5 warnings / 0 errors in 2 files ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc --enable=211") end) it("allows using cli-specific options in top level config", function() assert.equal([[Files: 2 Warnings: 14 Errors: 0 Quiet: 0 Color: false Codes: true ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/cli_specific_config.luacheckrc --std=lua52") end) it("uses exclude_files option", function() assert.equal([[ Checking spec/samples/argparse.lua 6 warnings Checking spec/samples/compat.lua 4 warnings 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 Checking spec/samples/inline_options.lua 7 warnings / 2 errors Checking spec/samples/python_code.lua 1 error Checking spec/samples/read_globals.lua 5 warnings Checking spec/samples/read_globals_inline_options.lua 3 warnings Checking spec/samples/redefined.lua 7 warnings Checking spec/samples/unused_code.lua 9 warnings Checking spec/samples/unused_secondaries.lua 4 warnings Total: 53 warnings / 4 errors in 14 files ]], get_output "spec/samples --config=spec/configs/exclude_files_config.luacheckrc -qq") end) it("loads exclude_files option correctly from upper directory", function() assert.equal([[ Checking argparse.lua 6 warnings Checking compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings Checking globals.lua 2 warnings Checking inline_options.lua 7 warnings / 2 errors Checking python_code.lua 1 error Checking read_globals.lua 5 warnings Checking read_globals_inline_options.lua 3 warnings Checking redefined.lua 7 warnings Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings Total: 53 warnings / 4 errors in 14 files ]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq", "spec/samples/")) end) it("combines excluded files from config and cli", function() assert.equal([[ Checking argparse.lua 6 warnings Checking compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings Checking globals.lua 2 warnings Checking inline_options.lua 7 warnings / 2 errors Checking python_code.lua 1 error Checking redefined.lua 7 warnings Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings Total: 45 warnings / 4 errors in 12 files ]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files './read*'", "spec/samples/")) end) it("allows defining custom stds", function() assert.equal([[ Checking spec/samples/globals.lua 2 warnings spec/samples/globals.lua:1:15: accessing undefined variable rawlen spec/samples/globals.lua:1:22: accessing undefined variable tostring Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc") assert.equal([[ Checking spec/samples/globals.lua 2 warnings spec/samples/globals.lua:1:1: accessing undefined variable print spec/samples/globals.lua:1:15: accessing undefined variable rawlen Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=other_std") assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:15: accessing undefined variable rawlen Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=+other_std") assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:7: accessing undefined variable setfenv Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=lua52") end) it("allows importing options with require", function() assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:7: (W113) accessing undefined variable setfenv Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/import_config.luacheckrc") end) end) describe("error handling", function() it("raises critical error on config with syntax errors", function() assert.equal([[ Critical error: Couldn't load configuration from spec/configs/bad_config.luacheckrc: syntax error ]], get_output "spec/samples/empty.lua --config=spec/configs/bad_config.luacheckrc") assert.equal(3, get_exitcode "spec/samples/empty.lua --config=spec/configs/bad_config.luacheckrc") end) it("raises critical error on non-existent config", function() assert.equal([[ Critical error: Couldn't find configuration file spec/configs/config_404.luacheckrc ]], get_output "spec/samples/empty.lua --config=spec/configs/config_404.luacheckrc") assert.equal(3, get_exitcode "spec/samples/empty.lua --config=spec/configs/config_404.luacheckrc") end) end) describe("overwriting", function() it("prioritizes CLI options over config", function() assert.equal(1, get_exitcode "spec/samples/compat.lua --config=spec/configs/cli_override_config.luacheckrc --new-globals foo") end) it("prioritizes CLI options over config overrides", function() assert.equal(1, get_exitcode "spec/samples/compat.lua --config=spec/configs/cli_override_file_config.luacheckrc --new-globals foo") end) it("concats array-like options from config and CLI", function() assert.equal([[ Checking spec/samples/globals.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/global_config.luacheckrc --globals tostring") end) end) end) end) luacheck-0.13.0/spec/lexer_spec.lua0000644000175000017500000004726612642521554016217 0ustar vsevavsevalocal lexer = require "luacheck.lexer" local function get_tokens(source) local lexer_state = lexer.new_state(source) local tokens = {} repeat local token = {} token.token, token.token_value, token.line, token.column, token.offset = lexer.next_token(lexer_state) tokens[#tokens+1] = token until token.token == "eof" return tokens end local function get_token(source) local lexer_state = lexer.new_state(source) local token = {} token.token, token.token_value = lexer.next_token(lexer_state) return token end local function maybe_error(lexer_state) local ok, err, line, column, _, end_column = lexer.next_token(lexer_state) return not ok and {msg = err, line = line, column = column, end_column = end_column} end local function get_error(source) return maybe_error(lexer.new_state(source)) end local function get_last_error(source) local lexer_state = lexer.new_state(source) local err repeat err = maybe_error(lexer_state) until err return err end describe("lexer", function() describe("quote", function() it("quotes strings", function() assert.equal("'foo'", lexer.quote("foo")) end) it("escapes not printable characters", function() assert.equal([['\0\1foo \240bar\127\10']], lexer.quote("\0\1foo \240bar\127\n")) end) end) it("parses EOS correctly", function() assert.same({token = "eof"}, get_token(" ")) end) it("parses names correctly", function() assert.same({token = "name", token_value = "foo"}, get_token("foo")) assert.same({token = "name", token_value = "_"}, get_token("_")) assert.same({token = "name", token_value = "foo1_2"}, get_token("foo1_2")) assert.same({token = "name", token_value = "foo"}, get_token("foo!")) end) it("parses keywords correctly", function() assert.same({token = "do"}, get_token("do")) assert.same({token = "goto"}, get_token("goto fail;")) end) it("parses operators and special tokens correctly", function() assert.same({token = "="}, get_token("= =")) assert.same({token = "=="}, get_token("==")) assert.same({token = "<"}, get_token("< =")) assert.same({token = "<="}, get_token("<=")) assert.same({token = "<<"}, get_token("<<")) assert.same({token = ">"}, get_token("> =")) assert.same({token = ">="}, get_token(">=")) assert.same({token = ">>"}, get_token(">>")) assert.same({token = "/"}, get_token("/ /")) assert.same({token = "//"}, get_token("//")) assert.same({token = "."}, get_token(".?.")) assert.same({token = "."}, get_token(".")) assert.same({token = ".."}, get_token("..%")) assert.same({token = "...", token_value = "..."}, get_token("...")) assert.same({token = ":"}, get_token(":.:")) assert.same({token = "::"}, get_token("::.")) end) it("parses single character tokens correctly", function() assert.same({token = "("}, get_token("((")) assert.same({token = "["}, get_token("[x]")) assert.same({token = "$"}, get_token("$$$")) end) describe("when parsing short strings", function() it("parses empty short strings correctly", function() assert.same({token = "string", token_value = ""}, get_token([[""]])) assert.same({token = "string", token_value = ""}, get_token([['']])) end) it("parses short strings containing quotation marks correctly", function() assert.same({token = "string", token_value = "'"}, get_token([["'"]])) assert.same({token = "string", token_value = '"'}, get_token([['"']])) end) it("parses simple short strings correctly", function() assert.same({token = "string", token_value = "foo"}, get_token([["foo"]])) end) it("parses simple escape sequences correctly", function() assert.same({token = "string", token_value = "\r\n"}, get_token([["\r\n"]])) assert.same({token = "string", token_value = "foo\\bar"}, get_token([["foo\\bar"]])) assert.same({token = "string", token_value = "a\'\'b\"\""}, get_token([["a\'\'b\"\""]])) end) it("parses escaped newline correctly", function() assert.same({token = "string", token_value = "foo \nbar"}, get_token([["foo \ bar"]])) assert.same({token = "string", token_value = "foo \n\n\nbar"}, get_token([["foo \ \ \ bar"]])) end) it("parses \\z correctly", function() assert.same({token = "string", token_value = "foo "}, get_token([["foo \z"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \zbar"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \z bar"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \z bar\z "]])) end) it("parses decimal escape sequences correctly", function() assert.same({token = "string", token_value = "\0buffer exploit"}, get_token([["\0buffer exploit"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo b\97r"]])) assert.same({token = "string", token_value = "\1234"}, get_token([["\1234"]])) assert.same({line = 1, column = 2, end_column = 5, msg = "invalid decimal escape sequence '\\300'"}, get_error([["\300"]])) assert.same({line = 1, column = 2, end_column = 2, msg = "invalid escape sequence '\\'"}, get_error([["\]])) end) it("parses hexadecimal escape sequences correctly", function() assert.same({token = "string", token_value = "\0buffer exploit"}, get_token([["\x00buffer exploit"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo\x20bar"]])) assert.same({token = "string", token_value = "jj"}, get_token([["\x6a\x6A"]])) assert.same({line = 1, column = 2, end_column = 3, msg = "invalid escape sequence '\\X'"}, get_error([["\XFF"]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\x\"'"}, get_error([["\x"]])) assert.same({line = 1, column = 2, end_column = 5, msg = "invalid hexadecimal escape sequence '\\x1\"'"}, get_error([["\x1"]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\x1'"}, get_error([["\x1]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\xx'"}, get_error([["\xxx"]])) end) it("parses utf-8 escape sequences correctly", function() assert.same({token = "string", token_value = "\0\0"}, get_token([["\u{0}\u{00000000}"]])) assert.same({token = "string", token_value = "\0\127"}, get_token([["\u{0}\u{7F}"]])) assert.same({token = "string", token_value = "\194\128\223\191"}, get_token([["\u{80}\u{7fF}"]])) assert.same({token = "string", token_value = "\224\160\128\239\191\191"}, get_token([["\u{800}\u{FFFF}"]])) assert.same({token = "string", token_value = "\240\144\128\128\244\143\191\191"}, get_token([["\u{10000}\u{10FFFF}"]])) assert.same({line = 1, column = 2, end_column = 10, msg = "invalid UTF-8 escape sequence '\\u{110000'"}, get_error([["\u{110000}"]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\u\"'"}, get_error([["\u"]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\un'"}, get_error([["\unrelated"]])) assert.same({line = 1, column = 2, end_column = 7, msg = "invalid UTF-8 escape sequence '\\u{11u'"}, get_error([["\u{11unrelated"]])) assert.same({line = 1, column = 2, end_column = 6, msg = "invalid UTF-8 escape sequence '\\u{11'"}, get_error([["\u{11]])) assert.same({line = 1, column = 2, end_column = 5, msg = "invalid UTF-8 escape sequence '\\u{u'"}, get_error([["\u{unrelated}"]])) assert.same({line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\u{'"}, get_error([["\u{]])) end) it("detects unknown escape sequences", function() assert.same({line = 1, column = 2, end_column = 3, msg = "invalid escape sequence '\\c'"}, get_error([["\c"]])) end) it("detects unfinished strings", function() assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([["]])) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([["']])) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([[" "]])) end) end) describe("when parsing long strings", function() it("parses empty long strings correctly", function() assert.same({token = "string", token_value = ""}, get_token("[[]]")) assert.same({token = "string", token_value = ""}, get_token("[===[]===]")) end) it("parses simple long strings correctly", function() assert.same({token = "string", token_value = "foo"}, get_token("[[foo]]")) assert.same({token = "string", token_value = "'foo'\n'bar'\n"}, get_token("[===['foo'\n'bar'\n]===]")) end) it("skips first newline", function() assert.same({token = "string", token_value = ""}, get_token("[[\n]]")) assert.same({token = "string", token_value = "\n"}, get_token("[===[\n\n]===]")) end) it("ignores closing brackets of unrelated length", function() assert.same({token = "string", token_value = "]=] "}, get_token("[[]=] ]]")) assert.same({token = "string", token_value = "foo]]\n]=== ]]"}, get_token("[===[foo]]\n]=== ]]]===]")) end) it("detects invalid opening brackets", function() assert.same({line = 1, column = 1, end_column = 1, msg = "invalid long string delimiter"}, get_error("[=")) assert.same({line = 1, column = 1, end_column = 1, msg = "invalid long string delimiter"}, get_error("[=|")) end) it("detects unfinished long strings", function() assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long string"}, get_error("[=[\n")) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long string"}, get_error("[[]")) end) end) describe("when parsing numbers", function() it("parses decimal integers correctly", function() assert.same({token = "number", token_value = "0"}, get_token("0")) assert.same({token = "number", token_value = "123456789"}, get_token("123456789")) end) it("parses hexadecimal integers correctly", function() assert.same({token = "number", token_value = "0x0"}, get_token("0x0")) assert.same({token = "number", token_value = "0X0"}, get_token("0X0")) assert.same({token = "number", token_value = "0xFfab"}, get_token("0xFfab")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x")) end) it("parses decimal floats correctly", function() assert.same({token = "number", token_value = "0.0"}, get_token("0.0")) assert.same({token = "number", token_value = "0."}, get_token("0.")) assert.same({token = "number", token_value = ".1234"}, get_token(".1234")) end) it("parses hexadecimal floats correctly", function() assert.same({token = "number", token_value = "0xf.A"}, get_token("0xf.A")) assert.same({token = "number", token_value = "0x9."}, get_token("0x9.")) assert.same({token = "number", token_value = "0x.b"}, get_token("0x.b")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x.")) end) it("parses decimal floats with exponent correctly", function() assert.same({token = "number", token_value = "1.8e1"}, get_token("1.8e1")) assert.same({token = "number", token_value = ".8e-1"}, get_token(".8e-1")) assert.same({token = "number", token_value = "1.E+20"}, get_token("1.E+20")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e-")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8E+")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8ee")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e-e")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8E+i")) end) it("parses hexadecimal floats with exponent correctly", function() assert.same({token = "number", token_value = "0x1.8p1"}, get_token("0x1.8p1")) assert.same({token = "number", token_value = "0x.8P-1"}, get_token("0x.8P-1")) assert.same({token = "number", token_value = "0x1.p+20"}, get_token("0x1.p+20")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p-")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8P+")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8pF")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p-F")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p+LL")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x.p1")) end) it("parses 64 bits cdata literals correctly", function() assert.same({token = "number", token_value = "1LL"}, get_token("1LL")) assert.same({token = "number", token_value = "1ll"}, get_token("1ll")) assert.same({token = "number", token_value = "1Ll"}, get_token("1Ll")) assert.same({token = "number", token_value = "1lL"}, get_token("1lL")) assert.same({token = "number", token_value = "1ULL"}, get_token("1ULL")) assert.same({token = "number", token_value = "1uLl"}, get_token("1uLl")) assert.same({token = "number", token_value = "1LLu"}, get_token("1LLu")) assert.same({token = "number", token_value = "1"}, get_token("1L")) assert.same({token = "number", token_value = "1LL"}, get_token("1LLG")) assert.same({token = "number", token_value = "1"}, get_token("1LUL")) assert.same({token = "number", token_value = "0x1LL"}, get_token("0x1LL")) assert.same({token = "number", token_value = "1.0"}, get_token("1.0LL")) end) it("parses complex cdata literals correctly", function() assert.same({token = "number", token_value = "1i"}, get_token("1i")) assert.same({token = "number", token_value = "1I"}, get_token("1I")) assert.same({token = "number", token_value = "1"}, get_token("1j")) assert.same({token = "number", token_value = "1LL"}, get_token("1LLi")) assert.same({token = "number", token_value = "0x1i"}, get_token("0x1i")) assert.same({token = "number", token_value = "0x1.0i"}, get_token("0x1.0i")) end) end) it("parses short comments correctly", function() assert.same({token = "comment", token_value = ""}, get_token("--")) assert.same({token = "comment", token_value = "foo"}, get_token("--foo\nbar")) assert.same({token = "comment", token_value = "["}, get_token("--[")) assert.same({token = "comment", token_value = "[=foo"}, get_token("--[=foo\nbar")) end) it("parses long comments correctly", function() assert.same({token = "comment", token_value = ""}, get_token("--[[]]")) assert.same({token = "comment", token_value = ""}, get_token("--[[\n]]")) assert.same({token = "comment", token_value = "foo\nbar"}, get_token("--[[foo\nbar]]")) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long comment"}, get_error("--[=[]]")) end) it("provides correct location info", function() assert.same({ {token = "local", line = 1, column = 1, offset = 1}, {token = "function", line = 1, column = 7, offset = 7}, {token = "name", token_value = "foo", line = 1, column = 16, offset = 16}, {token = "(", line = 1, column = 19, offset = 19}, {token = "name", token_value = "bar", line = 1, column = 20, offset = 20}, {token = ")", line = 1, column = 23, offset = 23}, {token = "return", line = 2, column = 4, offset = 28}, {token = "name", token_value = "bar", line = 2, column = 11, offset = 35}, {token = ":", line = 2, column = 14, offset = 38}, {token = "name", token_value = "get_foo", line = 2, column = 15, offset = 39}, {token = "string", token_value = "long string\n", line = 2, column = 22, offset = 46}, {token = "end", line = 5, column = 1, offset = 66}, {token = "name", token_value = "print", line = 7, column = 1, offset = 71}, {token = "string", token_value = "123\n", line = 7, column = 7, offset = 77}, {token = "eof", line = 10, column = 1, offset = 105} }, get_tokens([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\z 2\z 3\n" ]])) end) it("provides correct location info for errors", function() assert.same({line = 7, column = 9, end_column = 10, msg = "invalid escape sequence '\\g'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\g 2\z 3\n" ]])) assert.same({line = 8, column = 9, end_column = 12, msg = "invalid decimal escape sequence '\\300'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\ 2\300 3\n" ]])) assert.same({line = 8, column = 1, end_column = 1, msg = "malformed number"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print ( 0xx) ]])) assert.same({line = 7, column = 7, end_column = 7, msg = "unfinished string"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\z 2\z 3\n ]])) end) it("parses minified source correctly", function() assert.same({ {token = "name", token_value = "a", line = 1, column = 1, offset = 1}, {token = ",", line = 1, column = 2, offset = 2}, {token = "name", token_value = "b", line = 1, column = 3, offset = 3}, {token = "=", line = 1, column = 4, offset = 4}, {token = "number", token_value = "4ll", line = 1, column = 5, offset = 5}, {token = "name", token_value = "f", line = 1, column = 8, offset = 8}, {token = "=", line = 1, column = 9, offset = 9}, {token = "string", token_value = "", line = 1, column = 10, offset = 10}, {token = "function", line = 1, column = 12, offset = 12}, {token = "name", token_value = "_", line = 1, column = 21, offset = 21}, {token = "(", line = 1, column = 22, offset = 22}, {token = ")", line = 1, column = 23, offset = 23}, {token = "return", line = 1, column = 24, offset = 24}, {token = "number", token_value = "1", line = 1, column = 31, offset = 31}, {token = "or", line = 1, column = 32, offset = 32}, {token = "string", token_value = "", line = 1, column = 34, offset = 34}, {token = "end", line = 1, column = 36, offset = 36}, {token = "eof", line = 1, column = 39, offset = 39} }, get_tokens("a,b=4llf=''function _()return 1or''end")) end) it("handles argparse sample", function() get_tokens(io.open("spec/samples/argparse.lua", "rb"):read("*a")) end) end) luacheck-0.13.0/spec/helper.lua0000644000175000017500000000270712642521554015334 0ustar vsevavsevalocal helper = {} local dir_sep = package.config:sub(1, 1) -- Return path to root directory when run from `path`. local function antipath(path) local _, level = path:gsub(dir_sep, "") return (".."..dir_sep):rep(level) end function helper.luacov_config(prefix) return { statsfile = prefix.."luacov.stats.out", modules = { luacheck = "src/luacheck/init.lua", ["luacheck.*"] = "src" }, exclude = { "bin/luacheck$", "luacheck/argparse$" } } end local luacov = package.loaded.luacov or package.loaded["luacov.runner"] -- Returns command that runs `luacheck` executable from `loc_path`. function helper.luacheck_command(loc_path) loc_path = loc_path or "." local prefix = antipath(loc_path) local cmd = ("cd %s && lua"):format(loc_path) -- Extend package.path to allow loading this helper and luacheck modules. cmd = cmd..(" -e 'package.path=[[%s?.lua;%ssrc%s?.lua;%ssrc%s?%sinit.lua;]]..package.path'"):format( prefix, prefix, dir_sep, prefix, dir_sep, dir_sep) if luacov then -- Launch luacov. cmd = cmd..(" -e 'require[[luacov.runner]](require[[spec.helper]].luacov_config([[%s]]))'"):format(prefix) end return ("%s %sbin%sluacheck.lua"):format(cmd, prefix, dir_sep) end function helper.before_command() if luacov then luacov.pause() end end function helper.after_command() if luacov then luacov.resume() end end return helper luacheck-0.13.0/spec/configs/0000755000175000017500000000000012642521554014774 5ustar vsevavsevaluacheck-0.13.0/spec/configs/override_config.luacheckrc0000644000175000017500000000020612642521554022164 0ustar vsevavsevafiles["spec/samples/bad_code.lua"] = { redefined = false, compat = true } files["spec/samples/unused_code.lua"].unused = false luacheck-0.13.0/spec/configs/invalid_override_config.luacheckrc0000644000175000017500000000006712642521554023677 0ustar vsevavsevaignore = {"211"} files["spec/foo.lua"].enable = "211" luacheck-0.13.0/spec/configs/import_config.luacheckrc0000644000175000017500000000010112642521554021651 0ustar vsevavsevacodes = true std = "lua51" return require "spec.configs.config" luacheck-0.13.0/spec/configs/config.lua0000644000175000017500000000003412642521554016741 0ustar vsevavsevareturn { std = "lua52" } luacheck-0.13.0/spec/configs/runtime_bad_config.luacheckrc0000644000175000017500000000001012642521554022627 0ustar vsevavseva(nil)() luacheck-0.13.0/spec/configs/bad_config.luacheckrc0000644000175000017500000000011712642521554021074 0ustar vsevavseva-- let's talk about ruby def method_missing(*args); args.join(" "); end -- wat luacheck-0.13.0/spec/configs/multioverride_config.luacheckrc0000644000175000017500000000025312642521554023241 0ustar vsevavsevaunused = false files["spec/samples/"] = { unused_args = true, enable = {"31"}, ignore = {"213"} } files["spec/samples/unused_code.lua"] = { enable = {"213"} } luacheck-0.13.0/spec/configs/cli_specific_config.luacheckrc0000644000175000017500000000046212642521554022765 0ustar vsevavsevacolor = false codes = true formatter = function(report, file_names, options) return ([[ Files: %d Warnings: %d Errors: %d Quiet: %d Color: %s Codes: %s]]):format(#file_names, report.warnings, report.errors, options.quiet, options.color and "true" or "false", options.codes and "true" or "false") end luacheck-0.13.0/spec/configs/exclude_files_config.luacheckrc0000644000175000017500000000011112642521554023153 0ustar vsevavsevaexclude_files = {"spec/samples/defined?.lua", "**/bad*.lua"} std = "min" luacheck-0.13.0/spec/configs/project/0000755000175000017500000000000012642521554016442 5ustar vsevavsevaluacheck-0.13.0/spec/configs/project/.luacheckrc0000644000175000017500000000016412642521554020550 0ustar vsevavsevafiles["nested/ab.lua"].ignore = {"a"} files["nested/nested/"].enable = {"a"} files["nested/nested/"].ignore = {"b"} luacheck-0.13.0/spec/configs/project/nested/0000755000175000017500000000000012642521554017724 5ustar vsevavsevaluacheck-0.13.0/spec/configs/project/nested/ab.lua0000644000175000017500000000001412642521554021004 0ustar vsevavsevaprint(a, b) luacheck-0.13.0/spec/configs/project/nested/nested/0000755000175000017500000000000012642521554021206 5ustar vsevavsevaluacheck-0.13.0/spec/configs/project/nested/nested/abc.lua0000644000175000017500000000001712642521554022434 0ustar vsevavsevaprint(a, b, c) luacheck-0.13.0/spec/configs/custom_stds_config.luacheckrc0000644000175000017500000000014412642521554022715 0ustar vsevavsevastds = { my_std = {"print", "setfenv"}, other_std = {"tostring", "setfenv"} } std = "my_std" luacheck-0.13.0/spec/configs/cli_override_config.luacheckrc0000644000175000017500000000006712642521554023020 0ustar vsevavsevaglobal = true globals = {"print", "setfenv", "rawlen"} luacheck-0.13.0/spec/configs/global_config.luacheckrc0000644000175000017500000000005112642521554021603 0ustar vsevavsevaglobals = {"print", "setfenv", "rawlen"} luacheck-0.13.0/spec/configs/cli_override_file_config.luacheckrc0000644000175000017500000000014512642521554024014 0ustar vsevavsevafiles["spec/samples/compat.lua"] = { global = true, globals = {"print", "setfenv", "rawlen"} } luacheck-0.13.0/spec/configs/invalid_config.luacheckrc0000644000175000017500000000001712642521554021773 0ustar vsevavsevaignore = "211" luacheck-0.13.0/spec/formatters/0000755000175000017500000000000012642521554015532 5ustar vsevavsevaluacheck-0.13.0/spec/formatters/custom_formatter.lua0000644000175000017500000000034312642521554021632 0ustar vsevavsevareturn function(report, file_names, options) return ([[ Files: %d Formatter: %s Quiet: %d Color: %s Codes: %s]]):format(#file_names, options.formatter, options.quiet, tostring(options.color), tostring(options.codes)) end luacheck-0.13.0/.luacov0000644000175000017500000000005712642521554013706 0ustar vsevavsevareturn require "spec.helper".luacov_config("") luacheck-0.13.0/docsrc/0000755000175000017500000000000012642521554013667 5ustar vsevavsevaluacheck-0.13.0/docsrc/conf.py0000644000175000017500000002027512642521554015174 0ustar vsevavseva# -*- coding: utf-8 -*- # # luacheck documentation build configuration file, created by # sphinx-quickstart on Thu Sep 4 16:25:37 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'luacheck' copyright = u'2014 - 2016, Peter Melnichenko' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.13.0' # The full version, including alpha/beta/rc tags. release = '0.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import os if os.environ.get('READTHEDOCS', None) != 'True': try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: pass # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'luacheckdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'luacheck.tex', u'luacheck Documentation', u'Peter Melnichenko', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'luacheck', u'luacheck Documentation', [u'Peter Melnichenko'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'luacheck', u'luacheck Documentation', u'Peter Melnichenko', 'luacheck', 'A simple Lua static analyzer.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False luacheck-0.13.0/docsrc/module.rst0000644000175000017500000000513712642521554015714 0ustar vsevavsevaLuacheck module =============== Use ``local luacheck = require "luacheck"`` to import ``luacheck`` module. It contains the following functions: * ``luacheck.get_report(source)``: Given source string, returns analysis data (a table). * ``luacheck.process_reports(reports, options)``: Processes array of analysis reports and applies options. ``reports[i]`` uses ``options``, ``options[i]``, ``options[i][1]``, ``options[i][2]``, ... as options, overriding each other in that order. Options table is a table with fields similar to config options; see :ref:`options`. Analysis reports with field ``fatal`` are ignored. ``process_reports`` returns final report, see :ref:`report`. * ``luacheck.check_strings(sources, options)``: Checks array of sources using options, returns final report. Tables with field ``fatal`` within ``sources`` array are ignored. * ``luacheck.check_files(files, options)``: Checks array of files using options, returns final report. Open file handles can passed instead of filenames, in which case they will be read till EOF and closed. * ``luacheck.get_message(issue)``: Returns a string message for an issue, see :ref:`report`. ``luacheck._VERSION`` contains Luacheck version as a string in ``MAJOR.MINOR.PATCH`` format. Using ``luacheck`` as a function is equivalent to calling ``luacheck.check_files``. .. _report: Report format ------------- A final report is an array of file reports plus fields ``warnings``, ``errors`` and ``fatals`` containing total number of warnings, errors and fatal errors, correspondingly. A file report is an array of issues (warnings or errors). If a fatal error occured while checking a file, its report will have ``fatal`` field containing error type. An issue is a table with field ``code`` indicating its type (see :doc:`warnings`), and fields ``line``, ``column`` and ``end_column`` pointing to the source of the warning. ``name`` field may contain name of relate variable. Issues of some types can also have additional fields: ===== ======================================================================================== Codes Additional fields ===== ======================================================================================== 011 ``msg`` field contains syntax error message. 111 ``module`` field indicates that assignment is to a non-module global variable. 211 ``func`` field indicates that unused variable is a function. 4.. ``prev_line`` and ``prev_column`` fields contain location of the overwritten definition. ===== ======================================================================================== Other fields may be present for internal reasons. luacheck-0.13.0/docsrc/warnings.rst0000644000175000017500000001417212642521554016256 0ustar vsevavsevaList of warnings ================ Warnings produced by Luacheck are categorized using three-digit warning codes. Warning codes can be displayed in CLI output using ``--codes`` CLI option or ``codes`` config option. Errors also have codes starting with zero. ==== ============================================= Code Description ==== ============================================= 011 A syntax error. 021 An invalid inline option. 022 An upaired inline push directive. 023 An upaired inline pop directive. 111 Setting an undefined global variable. 112 Mutating an undefined global variable. 113 Accessing an undefined global variable. 121 Setting a read-only global variable. 122 Mutating a read-only global variable. 131 Unused implicitly defined global variable. 211 Unused local variable. 212 Unused argument. 213 Unused loop variable. 221 Local variable is accessed but never set. 231 Local variable is set but never accessed. 232 An argument is set but never accessed. 233 Loop variable is set but never accessed. 311 Value assigned to a local variable is unused. 312 Value of an argument is unused. 313 Value of a loop variable is unused. 321 Accessing uninitialized local variable. 411 Redefining a local variable. 412 Redefining an argument. 413 Redefining a loop variable. 421 Shadowing a local variable. 422 Shadowing an argument. 423 Shadowing a loop variable. 431 Shadowing an upvalue. 432 Shadowing an upvalue argument. 433 Shadowing an upvalue loop variable. 511 Unreachable code. 512 Loop can be executed at most once. 521 Unused label. 531 Left-hand side of an assignment is too short. 532 Left-hand side of an assignment is too long. 541 An empty ``do`` ``end`` block. 542 An empty ``if`` branch. 551 An empty statement. ==== ============================================= Global variables ---------------- For each file, Luacheck builds list of defined globals which can be used there. By default only globals from Lua standard library are defined; custom globals can be added using ``--globals`` CLI option or ``globals`` config option, and version of standard library can be selected using ``--std`` CLI option or ``std`` config option. When an undefined global is set, mutated or accessed, Luacheck produces a warning. Read-only globals ^^^^^^^^^^^^^^^^^ By default, all standard globals except ``_G`` and ``package`` are marked as read-only, so that setting or mutating them produces a warning. Custom read-only globals can be added using ``--read-globals`` CLI option or ``read_globals`` config option. .. _implicitlydefinedglobals: Implicitly defined globals ^^^^^^^^^^^^^^^^^^^^^^^^^^ Luacheck can be configured to consider globals assigned under some conditions to be defined implicitly. When ``-d``/``--allow_defined`` CLI option or ``allow_defined`` config option is used, all assignments to globals define them; when ``-t``/``--allow_defined_top`` CLI option or ``allow_defined_top`` config option is used, assignments to globals in the top level function scope (also known as main chunk) define them. A warning is produced when an implicitly defined global is not accessed anywhere. .. _modules: Modules ^^^^^^^ Files can be marked as modules using ``-m``/``--module`` CLI option or ``module`` config option to simulate semantics of the deprecated `module `_ function. Globals implicitly defined inside a module are considired part of its interface, are not visible outside and are not reported as unused. Assignments to other globals are not allowed, even to defined ones. Unused variables and values --------------------------- Luacheck generates warnings for all unused local variables except one named ``_``. It also detects variables which are set but never accessed or accessed but never set. Unused values and uninitialized variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For each value assigned to a local variable, Luacheck computes set of expressions where it could be used. Warnings are produced for unused values (when a value can't be used anywhere) and for accessing uninitialized variables (when no values can reach an expression). E.g. in the following snippet value assigned to ``foo`` on line 1 is unused, and variable ``bar`` is uninitialized on line 9: .. code-block:: lua :linenos: local foo = expr1() local bar if condition() then foo = expr2() bar = expr3() else foo = expr4() print(bar) end return foo, bar .. _secondaryvaluesandvariables: Secondary values and variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused value assigned to a local variable is secondary if its origin is the last item on the RHS of assignment, and another value from that item is used. Secondary values typically appear when result of a function call is put into locals, and only some of them are later used. For example, here value assigned to ``b`` is secondary, value assigned to ``c`` is used, and value assigned to ``a`` is simply unused: .. code-block:: lua :linenos: local a, b, c = f(), g() return c A variable is secondary if all values assigned to it are secondary. In the snippet above, ``b`` is a secondary variable. Warnings related to unused secondary values and variables can be removed using ``-s``/``--no-unused-secondaries`` CLI option or ``unused_secondaries`` config option. Shadowing declarations ---------------------- Luacheck detects declarations of local variables shadowing previous declarations, unless the variable is named ``_``. If the previous declaration is in the same scope as the new one, it is called redefining. Note that it is **not** necessary to define a new local variable when overwriting an argument: .. code-block:: lua :linenos: local function f(x) local x = x or "default" -- bad end local function f(x) x = x or "default" -- good end Control flow and data flow issues --------------------------------- The following control flow and data flow issues are detected: * Unreachable code and loops that can be executed at most once (e.g. due to an unconditional break); * Unused labels; * Unbalanced assignments; * Empty blocks. * Empty statements (semicolons without preceding statements). luacheck-0.13.0/docsrc/index.rst0000644000175000017500000000040512642521554015527 0ustar vsevavsevaLuacheck documentation ====================== Contents: .. toctree:: warnings cli config inline module This is documentation for 0.13.0 version of `Luacheck `_, a linter for `Lua `_. luacheck-0.13.0/docsrc/config.rst0000644000175000017500000001235012642521554015667 0ustar vsevavsevaConfiguration file ================== ``luacheck`` tries to load configuration from ``.luacheckrc`` file in the current directory. If not found, it will look for it in the parent directory and so on, going up until it reaches file system root. Path to config can be set using ``--config`` option, in which case it will be used during recursive loading. Config loading can be disabled using ``--no-config`` flag. Config is simply a Lua script executed by ``luacheck``. It may set various options by assigning to globals or by returning a table with option names as keys. .. _options: Config options -------------- ====================== ======================================= ================== Option Type Default value ====================== ======================================= ================== ``color`` Boolean ``true`` ``codes`` Boolean ``false`` ``formatter`` String or function ``"default"`` ``cache`` Boolean or string ``false`` ``jobs`` Positive integer ``1`` ``exclude_files`` Array of strings ``{}`` ``include_files`` Array of strings (Include all files) ``global`` Boolean ``true`` ``unused`` Boolean ``true`` ``redefined`` Boolean ``true`` ``unused_args`` Boolean ``true`` ``unused_secondaries`` Boolean ``true`` ``self`` Boolean ``true`` ``std`` String or set of standard globals ``"_G"`` ``globals`` Array of strings ``{}`` ``new_globals`` Array of strings (Do not overwrite) ``read_globals`` Array of strings ``{}`` ``new_read_globals`` Array of strings (Do not overwrite) ``compat`` Boolean ``false`` ``allow_defined`` Boolean ``false`` ``allow_defined_top`` Boolean ``false`` ``module`` Boolean ``false`` ``ignore`` Array of patterns (see :ref:`patterns`) ``{}`` ``enable`` Array of patterns ``{}`` ``only`` Array of patterns (Do not filter) ``inline`` Boolean ``true`` ====================== ======================================= ================== An example of a config which makes ``luacheck`` ensure that only globals from the portable intersection of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0 are used, as well as disables detection of unused arguments: .. code-block:: lua :linenos: std = "min" ignore = {"212"} .. _custom_stds: Custom sets of globals ---------------------- ``std`` option allows setting a custom standard set of globals using a table. In that table, string keys are globals, and string in array part are read-only globals. Additionally, custom sets can be given names by mutating global ``stds`` variable. For example, when using `LPEG `_ library, it makes sense to access its functions tersely using globals. In that case, the following config allows removing false positives related to global access easily: .. code-block:: lua :linenos: stds.lpeg = require "lpeg" .. code-block:: lua :linenos: local lpeg = require "lpeg" local function parse1(...) -- This function only uses lpeg functions as globals. local _ENV = lpeg -- luacheck: std lpeg local digit, space = R "09", S " " -- ... end local function parse2(...) -- This function uses lpeg functions as well as standard globals. local _ENV = setmetatable({}, {__index = function(_, k) return _ENV[k] or lpeg[k] end}) -- luacheck: std +lpeg local digit, space = R "09", S " " local number = C(digit^1) / tonumber -- ... end Per-file and per-path overrides ------------------------------- The environment in which ``luacheck`` loads the config contains a special global ``files``. When checking a file ````, ``luacheck`` will override options from the main config with entries from ``files[]`` and ``files[]``, applying entries for shorter paths first. For example, the following config re-enables detection of unused arguments only for files in ``src/dir``, but not for ``src/dir/myfile.lua``, and allows using `Busted `_ globals within ``spec/``: .. code-block:: lua :linenos: std = "min" ignore = {"212"} files["src/dir"] = {enable = {"212"}} files["src/dir/myfile.lua"] = {ignore = {"212"}} files["spec"] = {std = "+busted"} Note that ``files`` table supports autovivification, so that .. code-block:: lua files["myfile.lua"].ignore = {"212"} and .. code-block:: lua files["myfile.lua"] = {ignore = {"212"}} are equivalent. luacheck-0.13.0/docsrc/inline.rst0000644000175000017500000000431112642521554015676 0ustar vsevavsevaInline options ============== Luacheck supports setting some options directly in the checked files using inline configuration comments. An inline configuration comment starts with ``luacheck:`` label, possibly after some whitespace. The body of the comment should contain comma separated options, where option invocation consists of its name plus space separated arguments. The following options are supported: ================== ============================================ Option Number of arguments ================== ============================================ global 0 unused 0 redefined 0 unused args 0 unused secondaries 0 self 0 compat 0 module 0 allow defined 0 allow defined top 0 std 1 globals 0+ new globals 0+ read globals 0+ new read globals 0+ ignore 0+ (without arguments everything is ignored) enable 1+ only 1+ ================== ============================================ Options that take no arguments can be prefixed with ``no`` to invert their meaning. E.g. ``--luacheck: no unused args`` disables unused argument warnings. Part of the file affected by inline option dependes on where it is placed. If there is any code on the line with the option, only that line is affected; otherwise, everthing till the end of the current closure is. In particular, inline options at the top of the file affect all of it: .. code-block:: lua :linenos: -- luacheck: globals g1 g2, ignore foo local foo = g1(g2) -- No warnings emitted. -- The following unused function is not reported. local function f() -- luacheck: ignore -- luacheck: globals g3 g3() -- No warning. end g3() -- Warning is emitted as the inline option defining g3 only affected function f. For fine-grained control over inline option visibility use ``luacheck: push`` and ``luacheck: pop`` directives: .. code-block:: lua :linenos: -- luacheck: push ignore foo foo() -- No warning. -- luacheck: pop foo() -- Warning is emitted. Inline options can be completely disabled using ``--no-inline`` CLI option or ``inline`` config option. luacheck-0.13.0/docsrc/cli.rst0000644000175000017500000003131012642521554015166 0ustar vsevavsevaCommand line interface ====================== ``luacheck`` program accepts files, directories and `rockspecs `_ as arguments. * Given a file, ``luacheck`` will check it. * Given ``-``, ``luacheck`` will check stdin. * Given a directory, ``luacheck`` will check all files within it, selecting only files with ``.lua`` extension unless ``--include-files`` option is used. This feature requires `LuaFileSystem `_ (installed automatically if LuaRocks was used to install Luacheck). * Given a rockspec (a file with ``.rockspec`` extension), ``luacheck`` will check all files with ``.lua`` extension mentioned in the rockspec in ``build.install.lua``, ``build.install.bin`` and ``build.modules`` tables. The output of ``luacheck`` consists of separate reports for each checked file and ends with a summary:: $ luacheck src Checking src/bad_code.lua 5 warnings src/bad_code.lua:3:16: unused variable helper src/bad_code.lua:3:23: unused variable length argument src/bad_code.lua:7:10: setting non-standard global variable embrace src/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 src/bad_code.lua:9:11: accessing undefined variable hepler Checking src/good_code.lua OK Checking src/python_code.lua 1 error src/python_code.lua:1:6: expected '=' near '__future__' Checking src/unused_code.lua 9 warnings src/unused_code.lua:3:18: unused argument baz src/unused_code.lua:4:8: unused loop variable i src/unused_code.lua:5:13: unused variable q src/unused_code.lua:7:11: unused loop variable a src/unused_code.lua:7:14: unused loop variable b src/unused_code.lua:7:17: unused loop variable c src/unused_code.lua:13:7: value assigned to variable x is unused src/unused_code.lua:14:1: value assigned to variable x is unused src/unused_code.lua:22:1: value assigned to variable z is unused Total: 14 warnings / 1 error in 4 files ``luacheck`` exits with 0 if no warnings or errors occured and with a positive number otherwise. .. _cliopts: Command line options -------------------- Short options that do not take an argument can be combined into one, so that ``-qqu`` is equivalent to ``-q -q -u``. For long options, both ``--option value`` or ``--option=value`` can be used. Options taking several arguments can be used several times; ``--ignore foo --ignore bar`` is equivalent to ``--ignore foo bar``. Note that options that may take several arguments, such as ``--globals``, should not be used immediately before positional arguments; given ``--globals foo bar file.lua``, ``luacheck`` will consider all ``foo``, ``bar`` and ``file.lua`` global and then panic as there are no file names left. ======================================= ============================================================================ Option Meaning ======================================= ============================================================================ ``-g | --no-global`` Filter out warnings related to global variables. ``-u | --no-unused`` Filter out warnings related to unused variables and values. ``-r | --no-redefined`` Filter out warnings related to redefined variables. ``-a | --no-unused-args`` Filter out warnings related to unused arguments and loop variables. ``-s | --no-unused-secondaries`` Filter out warnings related to unused variables set together with used ones. See :ref:`secondaryvaluesandvariables` ``--no-self`` Filter out warnings related to implicit ``self`` argument. ``--std `` Set standard globals. ```` can be one of: * ``_G`` - globals of the Lua interpreter ``luacheck`` runs on (default); * ``lua51`` - globals of Lua 5.1; * ``lua52`` - globals of Lua 5.2; * ``lua52c`` - globals of Lua 5.2 compiled with LUA_COMPAT_ALL; * ``lua53`` - globals of Lua 5.3; * ``lua53c`` - globals of Lua 5.3 compiled with LUA_COMPAT_5_2; * ``luajit`` - globals of LuaJIT 2.0; * ``ngx_lua`` - globals of Openresty `lua-nginx-module `_ with LuaJIT 2.0; * ``min`` - intersection of globals of Lua 5.1, Lua 5.2 and LuaJIT 2.0; * ``max`` - union of globals of Lua 5.1, Lua 5.2 and LuaJIT 2.0; * ``busted`` - globals added by Busted 2.0; * ``none`` - no standard globals. See :ref:`stds` ``--globals [] ...`` Add custom globals on top of standard ones. ``--read-globals [] ...`` Add read-only globals. ``--new-globals [] ...`` Set custom globals. Removes custom globals added previously. ``--new-read-globals [] ...`` Set read-only globals. Removes read-only globals added previously. ``-c | --compat`` Equivalent to ``--std max``. ``-d | --allow-defined`` Allow defining globals implicitly by setting them. See :ref:`implicitlydefinedglobals` ``-t | --allow-defined-top`` Allow defining globals implicitly by setting them in the top level scope. See :ref:`implicitlydefinedglobals` ``-m | --module`` Limit visibility of implicitly defined globals to their files. See :ref:`modules` ``--ignore | -i [] ...`` Filter out warnings matching patterns. ``--enable | -e [] ...`` Do not filter out warnings matching patterns. ``--only | -o [] ...`` Filter out warnings not matching patterns. ``--no-inline`` Disable inline options. ``--config `` Path to custom configuration file (default: ``.luacheckrc``). ``--no-config`` Do not look up custom configuration file. ``--filename `` Use another filename in output, for selecting configuration overrides and for file filtering. ``--exclude-files [] ...`` Do not check files matching these globbing patterns. Recursive globs such as ``**/*.lua`` are supported. ``--include-files [] ...`` Do not check files not matching these globbing patterns. ``--cache []`` Path to cache file. (default: ``.luacheckcache``). See :ref:`cache` ``--no-cache`` Do not use cache. ``-j | --jobs`` Check ```` files in parallel. Requires `LuaLanes `_. ``--formatter `` Use custom formatter. ```` must be a module name or one of: * ``TAP`` - Test Anything Protocol formatter; * ``JUnit`` - JUnit XML formatter; * ``plain`` - simple warning-per-line formatter; * ``default`` - standard formatter. ``-q | --quiet`` Suppress report output for files without warnings. * ``-qq`` - Suppress output of warnings. * ``-qqq`` - Only output summary. ``--codes`` Show warning codes. ``--ranges`` Show ranges of columns related to warnings. ``--no-color`` Do not colorize output. ``-v | --version`` Show version of Luacheck and its dependencies and exit. ``-h | --help`` Show help and exit. ======================================= ============================================================================ .. _patterns: Patterns -------- CLI options ``--ignore``, ``--enable`` and ``--only`` and corresponding config options allow filtering warnings using pattern matching on warning codes, variable names or both. If a pattern contains a slash, the part before slash matches warning code and the part after matches variable name. Otherwise, if a pattern contains a letter or underscore, it matches variable name. Otherwise, it matches warning code. E.g.: ======= ========================================================================= Pattern Matching warnings ======= ========================================================================= 4.2 Shadowing declarations of arguments or redefining them. .*_ Warnings related to variables with ``_`` suffix. 4.2/.*_ Shadowing declarations of arguments with ``_`` suffix or redefining them. ======= ========================================================================= Unless already anchored, patterns matching variable names are anchored at both sides and patterns matching warning codes are anchored at their beginnings. This allows one to filter warnings by category (e.g. ``--only 1`` focuses ``luacheck`` on global-related warnings). .. _stds: Sets of standard globals ------------------------ CLI option ``--stds`` allows combining built-in sets described above using ``+``. For example, ``--std max`` is equivalent to ``--std=lua51+lua52+lua53``. Leading plus sign adds new sets to default one instead of replacing it. For instance, ``--std +busted`` is suitable for checking test files that use `Busted `_ testing framework. Custom sets of globals can be defined by mutating global variable ``stds`` in config. See :ref:`custom_stds` Formatters ---------- CLI option ``--formatter`` allows selecting a custom formatter for ``luacheck`` output. A custom formatter is a Lua module returning a function with three arguments: report as returned by ``luacheck`` module (see :ref:`report`), array of file names and table of options. Options contain values assigned to ``quiet``, ``color``, ``limit``, ``codes``, ``ranges`` and ``formatter`` options in CLI or config. Formatter function must return a string. .. _cache: Caching ------- If LuaFileSystem is available, Luacheck can cache results of checking files. On subsequent checks, only files which have changed since the last check will be rechecked, improving run time significantly. Changing options (e.g. defining additional globals) does not invalidate cache. Caching can be enabled by using ``--cache `` option or ``cache`` config option. Using ``--cache`` without an argument or setting ``cache`` config option to ``true`` sets ``.luacheckcache`` as the cache file. Note that ``--cache`` must be used every time ``luacheck`` is run, not on the first run only. Stable interface for editor plugins and tools --------------------------------------------- Command-line interface of Luacheck can change between minor releases. Starting from 0.11.0 version, the following interface is guaranteed at least till 1.0.0 version, and should be used by tools using Luacheck output, e.g. editor plugins. * Luacheck should be started from the directory containing the checked file. * File can be passed through stdin using ``-`` as argument or using a temporary file. Real filename should be passed using ``--filename`` option. * Plain formatter should be used. It outputs one issue (warning or error) per line. * To get precise error location, ``--ranges`` option can be used. Each line starts with real filename (passed using ``--filename``), followed by ``::-:``, where ```` is line number on which issue occurred and ``-`` is inclusive range of columns of token related to issue. Numbering starts from 1. If ``--ranges`` is not used, end column and dash is not printed. * To get warning and error codes, ``--codes`` option can be used. For each line, substring between parentheses contains three digit issue code, prefixed with ``E`` for errors and ``W`` for warnings. Lack of such substring indicates a fatal error (e.g. I/O error). * The rest of the line is warning message. If compatibility with older Luacheck version is desired, output of ``luacheck --help`` can be used to get its version. If it contains string ``0..``, where ```` is at least 11 and ``patch`` is any number, interface described above should be used. luacheck-0.13.0/src/0000755000175000017500000000000012642521554013201 5ustar vsevavsevaluacheck-0.13.0/src/luacheck/0000755000175000017500000000000012642521554014760 5ustar vsevavsevaluacheck-0.13.0/src/luacheck/cache.lua0000644000175000017500000001742612642521554016540 0ustar vsevavsevalocal utils = require "luacheck.utils" local cache = {} -- Cache file contains check results for n unique filenames. -- Cache file consists of 3n+2 lines, the first line is empty and the second is cache format version. -- The rest are contain file records, 3 lines per file. -- For each file, first line is the filename, second is modification time, -- third is check result in lua table format. -- String fields are compressed into array indexes. cache.format_version = 1 local fields = { "code", "name", "line", "column", "end_column", "prev_line", "prev_column", "secondary", "self", "func", "filtered", "top", "read_only", "global", "filtered_111", "filtered_121", "filtered_131", "filtered_112", "filtered_122", "filtered_113","definition", "in_module", "msg" } -- Converts table with fields into table with indexes. local function compress(t) local res = {} for index, field in ipairs(fields) do res[index] = t[field] end return res end local function get_local_name(index) return string.char(index + (index > 26 and 70 or 64)) end -- Serializes event into buffer. -- strings is a table mapping string values to where they first occured or to name of local -- variable used to represent it. -- Array part contains representations of values saved into locals. local function serialize_event(buffer, strings, event) event = compress(event) table.insert(buffer, "{") local is_sparse local put_one for i = 1, #fields do local value = event[i] if not value then is_sparse = true else if put_one then table.insert(buffer, ",") end put_one = true if is_sparse then table.insert(buffer, ("[%d]="):format(i)) end if type(value) == "string" then local prev = strings[value] if type(prev) == "string" then -- There is a local with such value. table.insert(buffer, prev) elseif type(prev) == "number" and #strings < 52 then -- Value is used second time, put it into a local. table.insert(strings, ("%q"):format(value)) local local_name = get_local_name(#strings) buffer[prev] = local_name table.insert(buffer, local_name) strings[value] = local_name else table.insert(buffer, ("%q"):format(value)) strings[value] = #buffer end else table.insert(buffer, tostring(value)) end end end table.insert(buffer, "}") end -- Serializes check result into a string. function cache.serialize(events) local strings = {} local buffer = {"", "return {"} for i, event in ipairs(events) do if i > 1 then table.insert(buffer, ",") end serialize_event(buffer, strings, event) end table.insert(buffer, "}") if strings[1] then local names = {} for index in ipairs(strings) do table.insert(names, get_local_name(index)) end buffer[1] = "local " .. table.concat(names, ",") .. "=" .. table.concat(strings, ",") .. ";" end return table.concat(buffer) end -- Returns array of triplets of lines from cache fh. local function read_triplets(fh) local res = {} while true do local filename = fh:read() if filename then local mtime = fh:read() or "" local cached = fh:read() or "" table.insert(res, {filename, mtime, cached}) else break end end return res end -- Writes cache triplets into fh. local function write_triplets(fh, triplets) for _, triplet in ipairs(triplets) do fh:write(triplet[1], "\n") fh:write(triplet[2], "\n") fh:write(triplet[3], "\n") end end -- Converts table with indexes into table with fields. local function decompress(t) local res = {} for index, field in ipairs(fields) do res[field] = t[index] end return res end -- Loads cached results from string, returns results or nil. local function load_cached(cached) local func = utils.load(cached, {}) if not func then return end local ok, res = pcall(func) if not ok then return end if type(res) ~= "table" then return end local decompressed = {} for i, event in ipairs(res) do if type(event) ~= "table" then return end decompressed[i] = decompress(event) end return decompressed end local function check_version_header(fh) return fh:read() == "" and tonumber(fh:read()) == cache.format_version end local function write_version_header(fh) fh:write("\n", tostring(cache.format_version), "\n") end -- Loads cache for filenames given mtimes from cache cache_filename. -- Returns table mapping filenames to cached check results. -- On corrupted cache returns nil, on version mismatch returns {}. function cache.load(cache_filename, filenames, mtimes) local fh = io.open(cache_filename, "rb") if not fh then return {} end if not check_version_header(fh) then fh:close() return {} end local result = {} local not_yet_found = utils.array_to_set(filenames) while next(not_yet_found) do local filename = fh:read() if not filename then fh:close() return result end local mtime = fh:read() local cached = fh:read() if not mtime or not cached then fh:close() return end mtime = tonumber(mtime) if not mtime then fh:close() return end if not_yet_found[filename] then if mtimes[not_yet_found[filename]] == mtime then result[filename] = load_cached(cached) if result[filename] == nil then fh:close() return end end not_yet_found[filename] = nil end end fh:close() return result end -- Updates cache at cache_filename with results for filenames. -- Returns success flag + whether update was append-only. function cache.update(cache_filename, filenames, mtimes, results) local old_triplets = {} local can_append = false local fh = io.open(cache_filename, "rb") if fh then if check_version_header(fh) then old_triplets = read_triplets(fh) can_append = true end fh:close() end local filename_set = utils.array_to_set(filenames) local old_filename_set = {} -- Update old cache for files which got a new result. for i, triplet in ipairs(old_triplets) do old_filename_set[triplet[1]] = true local file_index = filename_set[triplet[1]] if file_index then can_append = false old_triplets[i][2] = mtimes[file_index] old_triplets[i][3] = cache.serialize(results[file_index]) end end local new_triplets = {} for _, filename in ipairs(filenames) do -- Use unique index (there could be duplicate filenames). local file_index = filename_set[filename] if file_index and not old_filename_set[filename] then table.insert(new_triplets, { filename, mtimes[file_index], cache.serialize(results[file_index]) }) -- Do not save result for this filename again. filename_set[filename] = nil end end if can_append then if #new_triplets > 0 then fh = io.open(cache_filename, "ab") if not fh then return false end write_triplets(fh, new_triplets) fh:close() end else fh = io.open(cache_filename, "wb") if not fh then return false end write_version_header(fh) write_triplets(fh, old_triplets) write_triplets(fh, new_triplets) fh:close() end return true, can_append end return cache luacheck-0.13.0/src/luacheck/multithreading.lua0000644000175000017500000000266112642521554020510 0ustar vsevavsevalocal utils = require "luacheck.utils" local multithreading = {} local ok, lanes = pcall(require, "lanes") ok = ok and pcall(lanes.configure) multithreading.has_lanes = ok multithreading.lanes = lanes if not ok then return multithreading end -- Worker thread reads pairs {outkey, arg} from inkey channel of linda, -- applies func to arg and sends result to outkey channel of linda -- until arg is nil. local function worker_task(linda, inkey, func) while true do local _, pair = linda:receive(nil, inkey) local outkey, arg = pair[1], pair[2] if arg == nil then return true end linda:send(nil, outkey, func(arg)) end end local worker_gen = lanes.gen("*", worker_task) -- Maps func over array, performing at most jobs calls in parallel. function multithreading.pmap(func, array, jobs) jobs = math.min(jobs, #array) if jobs < 2 then return utils.map(func, array) end local workers = {} local linda = lanes.linda() for i = 1, jobs do workers[i] = worker_gen(linda, 0, func) end for i, item in ipairs(array) do linda:send(nil, 0, {i, item}) end for _ = 1, jobs do linda:send(nil, 0, {}) end local results = {} for i in ipairs(array) do local _, result = linda:receive(nil, i) results[i] = result end for _, worker in ipairs(workers) do assert(worker:join()) end return results end return multithreading luacheck-0.13.0/src/luacheck/core_utils.lua0000644000175000017500000000461712642521554017643 0ustar vsevavsevalocal 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. -- 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] if not callback(line, stack_set, index, item, ...) and item then level = level + 1 stack[level] = index stack_set[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 index = backlog[level] backlog[level] = nil end end end local function once_per_item_callback_adapter(line, _, index, item, visited, callback, ...) if visited[index] then return true 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, ...) end -- Given a "global set" warning, return whether it is an implicit definition. function core_utils.is_definition(opts, warning) return opts.allow_defined or (opts.allow_defined_top and warning.top) end local function location_comparator(event1, event2) -- If two events share location, neither can be an invalid comment event. -- However, they can be equal by identity due to the way table.sort is implemented. return event1.line < event2.line or event1.line == event2.line and (event1.column < event2.column or event1.column == event2.column and event1.code and event1.code < event2.code) end function core_utils.sort_by_location(array) table.sort(array, location_comparator) end return core_utils luacheck-0.13.0/src/luacheck/linearize.lua0000644000175000017500000003653312642521554017457 0ustar vsevavsevalocal lexer = require "luacheck.lexer" local utils = require "luacheck.utils" local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"}) -- Who needs classes anyway. local function new_line() return { accessed_upvalues = {}, -- Maps variables to arrays of accessing items. set_upvalues = {}, -- Maps variables to arays of setting items. lines = {}, items = utils.Stack() } end local function new_scope(line) return { vars = {}, labels = {}, gotos = {}, line = line } end local function new_var(line, node, type_) return { name = node[1], location = node.location, type = type_, self = node.implicit, line = line, scope_start = line.items.size + 1, values = {} } end local function new_value(var_node, value_node, is_init) return { var = var_node.var, location = var_node.location, type = value_node and value_node.tag == "Function" and "func" or (is_init and var_node.var.type or "var"), initial = is_init, empty = is_init and not value_node and (var_node.var.type == "var") } end local function new_label(line, name, location, end_column) return { name = name, location = location, end_column = end_column, index = line.items.size + 1 } end local function new_goto(name, jump, location) return { name = name, jump = jump, location = location } end local function new_jump_item(is_conditional) return { tag = is_conditional and "Cjump" or "Jump" } end local function new_eval_item(expr) return { tag = "Eval", expr = expr, location = expr.location, token = expr.first_token, accesses = {}, used_values = {}, lines = {} } end local function new_noop_item(node, loop_end) return { tag = "Noop", location = node.location, token = node.first_token, loop_end = loop_end } end local function new_local_item(lhs, rhs, location, token) return { tag = "Local", lhs = lhs, rhs = rhs, location = location, token = token, accesses = rhs and {}, used_values = rhs and {}, lines = rhs and {} } end local function new_set_item(lhs, rhs, location, token) return { tag = "Set", lhs = lhs, rhs = rhs, location = location, token = token, accesses = {}, used_values = {}, lines = {} } end local function is_unpacking(node) return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke" end local LinState = utils.class() function LinState:__init(chstate) self.chstate = chstate self.lines = utils.Stack() self.scopes = utils.Stack() end function LinState:enter_scope() self.scopes:push(new_scope(self.lines.top)) end function LinState:leave_scope() local left_scope = self.scopes:pop() local prev_scope = self.scopes.top for _, goto_ in ipairs(left_scope.gotos) do local label = left_scope.labels[goto_.name] if label then goto_.jump.to = label.index label.used = true else if not prev_scope or prev_scope.line ~= self.lines.top then if goto_.name == "break" then lexer.syntax_error(goto_.location, goto_.location.column + 4, "'break' is not inside a loop") else lexer.syntax_error(goto_.location, goto_.location.column + 3, ("no visible label '%s'"):format(goto_.name)) end end table.insert(prev_scope.gotos, goto_) end end for name, label in pairs(left_scope.labels) do if not label.used and not pseudo_labels[name] then self.chstate:warn_unused_label(label) end end for _, var in pairs(left_scope.vars) do var.scope_end = self.lines.top.items.size end end function LinState:register_var(node, type_) local var = new_var(self.lines.top, node, type_) local prev_var = self:resolve_var(var.name) if prev_var then local same_scope = self.scopes.top.vars[var.name] self.chstate:warn_redefined(var, prev_var, same_scope) if same_scope then prev_var.scope_end = self.lines.top.items.size end end self.scopes.top.vars[var.name] = var node.var = var return var end function LinState:register_vars(nodes, type_) for _, node in ipairs(nodes) do self:register_var(node, type_) end end function LinState:resolve_var(name) for _, scope in utils.ripairs(self.scopes) do local var = scope.vars[name] if var then return var end end end function LinState:check_var(node, action) local var = self:resolve_var(node[1]) if not var then self.chstate:warn_global(node, action, self.lines.size == 1) else node.var = var end return var end function LinState:register_label(name, location, end_column) if self.scopes.top.labels[name] then assert(not pseudo_labels[name]) lexer.syntax_error(location, end_column, ("label '%s' already defined on line %d"):format( name, self.scopes.top.labels[name].location.line)) end self.scopes.top.labels[name] = new_label(self.lines.top, name, location, end_column) end -- `node` is assignment node (`Local or `Set). function LinState:check_balance(node) if node[2] then if #node[1] < #node[2] then self.chstate:warn_unbalanced(node.equals_location, true) elseif (#node[1] > #node[2]) and node.tag ~= "Local" and not is_unpacking(node[2][#node[2]]) then self.chstate:warn_unbalanced(node.equals_location) end end end function LinState:check_empty_block(block) if #block == 0 then self.chstate:warn_empty_block(block.location, block.tag == "Do") end end function LinState:emit(item) self.lines.top.items:push(item) end function LinState:emit_goto(name, is_conditional, location) local jump = new_jump_item(is_conditional) self:emit(jump) table.insert(self.scopes.top.gotos, new_goto(name, jump, location)) end local tag_to_boolean = { Nil = false, False = false, True = true, Number = true, String = true, Table = true, Function = true } -- Emits goto that jumps to ::name:: if bool(cond_node) == false. function LinState:emit_cond_goto(name, cond_node) local cond_bool = tag_to_boolean[cond_node.tag] if cond_bool ~= true then self:emit_goto(name, cond_bool ~= false) end end function LinState:emit_noop(node, loop_end) self:emit(new_noop_item(node, loop_end)) end function LinState:emit_stmt(stmt) self["emit_stmt_" .. stmt.tag](self, stmt) end function LinState:emit_stmts(stmts) for _, stmt in ipairs(stmts) do self:emit_stmt(stmt) end end function LinState:emit_block(block) self:enter_scope() self:emit_stmts(block) self:leave_scope() end function LinState:emit_stmt_Do(node) self:check_empty_block(node) self:emit_noop(node) self:emit_block(node) end function LinState:emit_stmt_While(node) self:emit_noop(node) self:enter_scope() self:register_label("do") self:emit_expr(node[1]) self:emit_cond_goto("break", node[1]) self:emit_block(node[2]) self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Repeat(node) self:emit_noop(node) self:enter_scope() self:register_label("do") self:enter_scope() self:emit_stmts(node[1]) self:emit_expr(node[2]) self:leave_scope() self:emit_cond_goto("do", node[2]) self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Fornum(node) self:emit_noop(node) self:emit_expr(node[2]) self:emit_expr(node[3]) if node[5] then self:emit_expr(node[4]) end self:enter_scope() self:register_label("do") self:emit_goto("break", true) self:enter_scope() self:emit(new_local_item({node[1]})) self:register_var(node[1], "loopi") self:emit_stmts(node[5] or node[4]) self:leave_scope() self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Forin(node) self:emit_noop(node) self:emit_exprs(node[2]) self:enter_scope() self:register_label("do") self:emit_goto("break", true) self:enter_scope() self:emit(new_local_item(node[1])) self:register_vars(node[1], "loop") self:emit_stmts(node[3]) self:leave_scope() self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_If(node) self:emit_noop(node) self:enter_scope() for i = 1, #node - 1, 2 do self:enter_scope() self:emit_expr(node[i]) self:emit_cond_goto("else", node[i]) self:check_empty_block(node[i + 1]) self:emit_block(node[i + 1]) self:emit_goto("end") self:register_label("else") self:leave_scope() end if #node % 2 == 1 then self:check_empty_block(node[#node]) self:emit_block(node[#node]) end self:register_label("end") self:leave_scope() end function LinState:emit_stmt_Label(node) self:register_label(node[1], node.location, node.end_column) end function LinState:emit_stmt_Goto(node) self:emit_noop(node) self:emit_goto(node[1], false, node.location) end function LinState:emit_stmt_Break(node) self:emit_goto("break", false, node.location) end function LinState:emit_stmt_Return(node) self:emit_noop(node) self:emit_exprs(node) self:emit_goto("return") end function LinState:emit_expr(node) local item = new_eval_item(node) self:scan_expr(item, node) self:emit(item) end function LinState:emit_exprs(exprs) for _, expr in ipairs(exprs) do self:emit_expr(expr) end end LinState.emit_stmt_Call = LinState.emit_expr LinState.emit_stmt_Invoke = LinState.emit_expr function LinState:emit_stmt_Local(node) self:check_balance(node) local item = new_local_item(node[1], node[2], node.location, node.first_token) self:emit(item) if node[2] then self:scan_exprs(item, node[2]) end self:register_vars(node[1], "var") end function LinState:emit_stmt_Localrec(node) local item = new_local_item({node[1]}, {node[2]}, node.location, node.first_token) self:register_var(node[1], "var") self:emit(item) self:scan_expr(item, node[2]) end function LinState:emit_stmt_Set(node) self:check_balance(node) local item = new_set_item(node[1], node[2], node.location, node.first_token) self:scan_exprs(item, node[2]) for _, expr in ipairs(node[1]) do if expr.tag == "Id" then local var = self:check_var(expr, "set") if var then self:register_upvalue_action(item, var, "set") end else assert(expr.tag == "Index") if expr[1].tag == "Id" and not self:resolve_var(expr[1][1]) then -- Warn about mutated global. self:check_var(expr[1], "mutate") else self:scan_expr(item, expr[1]) end self:scan_expr(item, expr[2]) end end self:emit(item) end function LinState:scan_expr(item, node) local scanner = self["scan_expr_" .. node.tag] if scanner then scanner(self, item, node) end end function LinState:scan_exprs(item, nodes) for _, node in ipairs(nodes) do self:scan_expr(item, node) end end function LinState:register_upvalue_action(item, var, action) local key = (action == "set") and "set_upvalues" or "accessed_upvalues" for _, line in utils.ripairs(self.lines) do if line == var.line then break end if not line[key][var] then line[key][var] = {} end table.insert(line[key][var], item) end end function LinState:mark_access(item, node) node.var.accessed = true if not item.accesses[node.var] then item.accesses[node.var] = {} end table.insert(item.accesses[node.var], node) self:register_upvalue_action(item, node.var, "access") end function LinState:scan_expr_Id(item, node) if self:check_var(node, "access") then self:mark_access(item, node) end end function LinState:scan_expr_Dots(item, node) local dots = self:check_var(node, "access") if not dots or dots.line ~= self.lines.top then lexer.syntax_error(node.location, node.location.column + 2, "cannot use '...' outside a vararg function") end self:mark_access(item, node) end LinState.scan_expr_Index = LinState.scan_exprs LinState.scan_expr_Call = LinState.scan_exprs LinState.scan_expr_Invoke = LinState.scan_exprs LinState.scan_expr_Paren = LinState.scan_exprs LinState.scan_expr_Pair = LinState.scan_exprs LinState.scan_expr_Table = LinState.scan_exprs function LinState:scan_expr_Op(item, node) self:scan_expr(item, node[2]) if node[3] then self:scan_expr(item, node[3]) end end -- 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() local line = self.lines.top for _, item in ipairs(line.items) do if item.tag == "Local" or item.tag == "Set" then item.set_variables = {} local is_init = item.tag == "Local" local unpacking_item -- Rightmost item of rhs which may unpack into several lhs items. if item.rhs then local last_rhs_item = item.rhs[#item.rhs] if is_unpacking(last_rhs_item) then unpacking_item = last_rhs_item end end local secondaries -- Array of values unpacked from rightmost rhs item. if unpacking_item and (#item.lhs > #item.rhs) then secondaries = {} end for i, node in ipairs(item.lhs) do local value if node.var then value = new_value(node, item.rhs and item.rhs[i] or unpacking_item, is_init) item.set_variables[node.var] = value table.insert(node.var.values, value) end if secondaries and (i >= #item.rhs) then if value then value.secondaries = secondaries table.insert(secondaries, value) else -- If one of secondary values is assigned to a global or index, -- it is considered used. secondaries.used = true end end end end end end function LinState:build_line(args, block) self.lines:push(new_line()) self:enter_scope() self:emit(new_local_item(args)) self:enter_scope() self:register_vars(args, "arg") self:emit_stmts(block) self:leave_scope() self:register_label("return") self:leave_scope() self:register_set_variables() local line = self.lines:pop() for _, prev_line in ipairs(self.lines) do table.insert(prev_line.lines, line) end return line end function LinState:scan_expr_Function(item, node) local line = self:build_line(node[1], node[2]) table.insert(item.lines, line) for _, nested_line in ipairs(line.lines) do table.insert(item.lines, nested_line) end end -- Builds linear representation of AST and returns it. -- Emits warnings: global, redefined/shadowed, unused label, unbalanced assignment, empty block. local function linearize(chstate, ast) local linstate = LinState(chstate) local line = linstate:build_line({{tag = "Dots", "..."}}, ast) assert(linstate.lines.size == 0) assert(linstate.scopes.size == 0) return line end return linearize luacheck-0.13.0/src/luacheck/version.lua0000644000175000017500000000123312642521554017147 0ustar vsevavsevalocal luacheck = require "luacheck" local fs = require "luacheck.fs" local multithreading = require "luacheck.multithreading" local version = {} version.luacheck = luacheck._VERSION if rawget(_G, "jit") then version.lua = rawget(_G, "jit").version else version.lua = _VERSION end if fs.has_lfs then version.lfs = fs.lfs._VERSION else version.lfs = "Not found" end if multithreading.has_lanes then version.lanes = multithreading.lanes.ABOUT.version else version.lanes = "Not found" end version.string = ([[ Luacheck: %s Lua: %s LuaFileSystem: %s LuaLanes: %s]]):format(version.luacheck, version.lua, version.lfs, version.lanes) return version luacheck-0.13.0/src/luacheck/config.lua0000644000175000017500000001620012642521554016727 0ustar vsevavsevalocal options = require "luacheck.options" local stds = require "luacheck.stds" local fs = require "luacheck.fs" local utils = require "luacheck.utils" local config = {} -- Config must support special metatables for some keys: -- autovivification for `files`, fallback to built-in stds for `stds`. local special_mts = { stds = {__index = stds}, files = {__index = function(files, key) files[key] = {} return files[key] end} } local function make_config_env_mt() local env_mt = {} local special_values = {} for key, mt in pairs(special_mts) do special_values[key] = setmetatable({}, mt) end function env_mt.__index(_, key) if special_mts[key] then return special_values[key] else return _G[key] end end function env_mt.__newindex(env, key, value) if special_mts[key] then if type(value) == "table" then setmetatable(value, special_mts[key]) end special_values[key] = value else rawset(env, key, value) end end return env_mt, special_values end local function make_config_env() local mt, special_values = make_config_env_mt() return setmetatable({}, mt), special_values end local function remove_env_mt(env, special_values) setmetatable(env, nil) utils.update(env, special_values) end local top_options = { color = utils.has_type("boolean"), codes = utils.has_type("boolean"), formatter = utils.either(utils.has_type("string"), utils.has_type("function")), cache = utils.either(utils.has_type("string"), utils.has_type("boolean")), jobs = function(x) return type(x) == "number" and math.floor(x) == x and x >= 1 end, files = utils.has_type("table"), stds = utils.has_type("table"), exclude_files = utils.array_of("string"), include_files = utils.array_of("string") } utils.update(top_options, options.all_options) options.add_order(top_options) -- Returns error or nil if options are valid. local function validate_options(option_set, opts) local ok, invalid_field = options.validate(option_set, opts) if not ok then if invalid_field then return ("invalid value of option '%s'"):format(invalid_field) else return "validation error" end end end -- Returns error or nil if config is valid. local function validate_config(conf) local top_err = validate_options(top_options, conf) if top_err then return top_err end for path, opts in pairs(conf.files) do if type(path) == "string" then local override_err = validate_options(options.all_options, opts) if override_err then return ("%s in options for path '%s'"):format(override_err, path) end end end end -- Returns table with field `paths` containing sorted normalize paths -- used in overrides and `options` mapping these paths to options. local function normalize_overrides(files, abs_conf_dir) local overrides = {paths = {}, options = {}} local orig_paths = {} for path in pairs(files) do table.insert(orig_paths, path) end table.sort(orig_paths) for _, orig_path in ipairs(orig_paths) do local path = fs.normalize(fs.join(abs_conf_dir, orig_path)) if not overrides.options[path] then table.insert(overrides.paths, path) end overrides.options[path] = files[orig_path] end table.sort(overrides.paths) return overrides end local function try_load(path) local src = utils.read_file(path) if not src then return end local func, err = utils.load(src, nil, "@"..path) return err or func end local function add_relative_loader(conf) local function loader(modname) local modpath = fs.join(conf.rel_dir, modname:gsub("%.", utils.dir_sep)) return try_load(modpath..".lua") or try_load(modpath..utils.dir_sep.."init.lua"), modname end table.insert(package.loaders or package.searchers, 1, loader) return loader end local function remove_relative_loader(loader) for i, func in ipairs(package.loaders or package.searchers) do if func == loader then table.remove(package.loaders or package.searchers, i) return end end end config.default_path = ".luacheckrc" config.empty_config = {empty = true} -- Loads config from path, returns config object or nil and error message. function config.load_config(path) local is_default_path = not path path = path or config.default_path local current_dir = fs.current_dir() local abs_conf_dir, rel_conf_dir = fs.find_file(current_dir, path) if not abs_conf_dir then if is_default_path then return config.empty_config else return nil, "Couldn't find configuration file "..path end end local conf = { abs_dir = abs_conf_dir, rel_dir = rel_conf_dir, cur_dir = current_dir } local conf_path = fs.join(rel_conf_dir, path) local env, special_values = make_config_env() local loader = add_relative_loader(conf) local load_ok, ret = utils.load_config(conf_path, env) remove_relative_loader(loader) if not load_ok then return nil, ("Couldn't load configuration from %s: %s error"):format(conf_path, ret) end -- Support returning some options from config instead of setting them as globals. -- This allows easily loading options from another file, for example using require. if type(ret) == "table" then utils.update(env, ret) end remove_env_mt(env, special_values) -- Update stds before validating config - std validation relies on that. if type(env.stds) == "table" then -- Ideally config shouldn't mutate global stds, not if `luacheck.config` becomes public -- interface. utils.update(stds, env.stds) end local err = validate_config(env) if err then return nil, ("Couldn't load configuration from %s: %s"):format(conf_path, err) end conf.options = env conf.overrides = normalize_overrides(env.files, abs_conf_dir) return conf end -- Adjusts path starting from config dir to start from current directory. function config.relative_path(conf, path) if conf.empty then return path else return fs.join(conf.rel_dir, path) end end -- Requires module from config directory. -- Returns success flag and module or error message. function config.relative_require(conf, modname) local loader if not conf.empty then loader = add_relative_loader(conf) end local ok, mod_or_err = pcall(require, modname) if not conf.empty then remove_relative_loader(loader) end return ok, mod_or_err end -- Returns top-level options. function config.get_top_options(conf) return conf.empty and {} or conf.options end -- Returns array of options for a file. function config.get_options(conf, file) if conf.empty then return {} end local res = {conf.options} if type(file) ~= "string" then return res end local path = fs.normalize(fs.join(conf.cur_dir, file)) for _, override_path in ipairs(conf.overrides.paths) do if fs.is_subpath(override_path, path) then table.insert(res, conf.overrides.options[override_path]) end end return res end return config luacheck-0.13.0/src/luacheck/check.lua0000644000175000017500000001254312642521554016545 0ustar vsevavsevalocal parse = require "luacheck.parser" local linearize = require "luacheck.linearize" local analyze = require "luacheck.analyze" local reachability = require "luacheck.reachability" local handle_inline_options = require "luacheck.inline_options" local core_utils = require "luacheck.core_utils" local utils = require "luacheck.utils" local function is_secondary(value) return value.secondaries and value.secondaries.used end local ChState = utils.class() function ChState:__init() self.warnings = {} end function ChState:warn(warning, implicit_self) if not warning.end_column then warning.end_column = implicit_self and warning.column or (warning.column + #warning.name - 1) end table.insert(self.warnings, warning) end local action_codes = { set = 1, mutate = 2, access = 3 } local type_codes = { var = 1, func = 1, arg = 2, loop = 3, loopi = 3 } function ChState:warn_global(node, action, is_top) self:warn({ code = "11" .. action_codes[action], name = node[1], line = node.location.line, column = node.location.column, top = is_top and (action == "set") or nil }) end -- W12* (read-only global) and W131 (unused global) are patched in during filtering. function ChState:warn_unused_variable(var) self:warn({ code = "21" .. type_codes[var.type], name = var.name, line = var.location.line, column = var.location.column, secondary = is_secondary(var.values[1]) or nil, func = (var.values[1].type == "func") or nil, self = var.self }, var.self) end function ChState:warn_unset(var) self:warn({ code = "221", name = var.name, line = var.location.line, column = var.location.column }) end function ChState:warn_unaccessed(var) -- Mark as secondary if all assigned values are secondary. -- It is guaranteed that there are at least two values. local secondary = true for _, value in ipairs(var.values) do if not value.empty and not is_secondary(value) then secondary = nil break end end self:warn({ code = "23" .. type_codes[var.type], name = var.name, line = var.location.line, column = var.location.column, secondary = secondary }, var.self) end function ChState:warn_unused_value(value) self:warn({ code = "31" .. type_codes[value.type], name = value.var.name, line = value.location.line, column = value.location.column, secondary = is_secondary(value) or nil }, value.type == "arg" and value.var.self) end function ChState:warn_uninit(node) self:warn({ code = "321", name = node[1], line = node.location.line, column = node.location.column }) end function ChState:warn_redefined(var, prev_var, same_scope) if var.name ~= "..." then self:warn({ code = "4" .. (same_scope and "1" or (var.line == prev_var.line and "2" or "3")) .. type_codes[prev_var.type], name = var.name, line = var.location.line, column = var.location.column, self = var.self and prev_var.self, prev_line = prev_var.location.line, prev_column = prev_var.location.column }, var.self) end end function ChState:warn_unreachable(location, unrepeatable, token) self:warn({ code = "51" .. (unrepeatable and "2" or "1"), line = location.line, column = location.column, end_column = location.column + #token - 1 }) end function ChState:warn_unused_label(label) self:warn({ code = "521", name = label.name, line = label.location.line, column = label.location.column, end_column = label.end_column }) end function ChState:warn_unbalanced(location, shorter_lhs) -- Location points to `=`. self:warn({ code = "53" .. (shorter_lhs and "1" or "2"), line = location.line, column = location.column, end_column = location.column }) end function ChState:warn_empty_block(location, do_end) -- Location points to `do`, `then` or `else`. self:warn({ code = "54" .. (do_end and "1" or "2"), line = location.line, column = location.column, end_column = location.column + (do_end and 1 or 3) }) end function ChState:warn_empty_statement(location) self:warn({ code = "551", line = location.line, column = location.column, end_column = location.column }) end local function check_or_throw(src) local ast, comments, code_lines, semicolons = parse(src) local chstate = ChState() local line = linearize(chstate, ast) for _, location in ipairs(semicolons) do chstate:warn_empty_statement(location) end analyze(chstate, line) reachability(chstate, line) handle_inline_options(ast, comments, code_lines, chstate.warnings) core_utils.sort_by_location(chstate.warnings) return chstate.warnings end --- Checks source. -- Returns an array of warnings and errors. Codes for errors start with "0". -- Syntax errors (with code "011") have message stored in .msg field. local function check(src) local warnings, err = utils.pcall(check_or_throw, src) if warnings then return warnings else local syntax_error = { code = "011", line = err.line, column = err.column, end_column = err.end_column, msg = err.msg } return {syntax_error} end end return check luacheck-0.13.0/src/luacheck/analyze.lua0000644000175000017500000001563212642521554017135 0ustar vsevavsevalocal core_utils = require "luacheck.core_utils" local function register_value(values_per_var, var, value) if not values_per_var[var] then values_per_var[var] = {} end table.insert(values_per_var[var], value) end local function add_resolution(item, var, value) register_value(item.used_values, var, value) value.used = true if value.secondaries then value.secondaries.used = true 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 register_value(line.last_live_values, var, value) return true end if not visited[index] and item.accesses and item.accesses[var] then add_resolution(item, var, value) end if stack[index] or (not visited[index] and (not in_scope(var, index) or item.set_variables and item.set_variables[var])) then if not item.live_values then item.live_values = {} end register_value(item.live_values, var, value) return true end if visited[index] then 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). 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) 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 if not item then live_values = line.last_live_values else live_values = item.live_values end if live_values then for var, accessing_items in pairs(subline.accessed_upvalues) 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(accessing_item, var, value) end end end end end end if not item then return true end if item.accesses then for var, setting_items in pairs(subline.set_upvalues) do if var.line == line then if item.accesses[var] then for _, setting_item in ipairs(setting_items) do add_resolution(item, var, setting_item.set_variables[var]) end end end end end end -- Updates variable resolution to account for closures and upvalues. local function propogate_closures(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, accessing_items in pairs(subline.accessed_upvalues) 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(accessing_item, var, setting_item.set_variables[var]) end end end end end end end end local function analyze_line(line) propogate_values(line) propogate_closures(line) end -- Emits warnings for variable. local function check_var(chstate, var) if #var.values == 1 then if not var.values[1].used then chstate:warn_unused_variable(var) elseif var.values[1].empty then var.empty = true chstate:warn_unset(var) end elseif not var.accessed then chstate:warn_unaccessed(var) else for _, value in ipairs(var.values) do if (not value.used) and (not value.empty) then chstate:warn_unused_value(value) end end end end -- Emits warnings for unused variables and values and unset variables in line. local function check_for_warnings(chstate, line) for _, item in ipairs(line.items) do if item.tag == "Local" then for var in pairs(item.set_variables) do -- Do not check implicit top level vararg. if var.location then check_var(chstate, var) end end end end end -- Finds reaching assignments for all variable accesses. -- Emits warnings: unused variable, unused value, unset variable. local function analyze(chstate, line) analyze_line(line) for _, nested_line in ipairs(line.lines) do analyze_line(nested_line) end check_for_warnings(chstate, line) for _, nested_line in ipairs(line.lines) do check_for_warnings(chstate, nested_line) end end return analyze luacheck-0.13.0/src/luacheck/filter.lua0000644000175000017500000002146012642521554016753 0ustar vsevavsevalocal options = require "luacheck.options" local core_utils = require "luacheck.core_utils" local utils = require "luacheck.utils" local filter = {} -- Returns array of normalized options, one for each file. local function get_normalized_opts(report, opts) local res = {} for i in ipairs(report) do local option_stack = {opts} if opts and opts[i] then option_stack[2] = opts[i] for _, nested_opts in ipairs(opts[i]) do table.insert(option_stack, nested_opts) end end res[i] = options.normalize(option_stack) end return res end -- A global is implicitly defined in a file if opts.allow_defined == true and it is set anywhere in the file, -- or opts.allow_defined_top == true and it is set in the top level function scope. -- By default, accessing and setting globals in a file is allowed for explicitly defined globals (standard and custom) -- for that file and implicitly defined globals from that file and all other files except modules (files with opts.module == true). -- Accessing other globals results in "accessing undefined variable" warning. -- Setting other globals results in "setting non-standard global variable" warning. -- Unused implicitly defined global results in "unused global variable" warning. -- For modules, accessing globals uses same rules as normal files, however, setting globals is only allowed for implicitly defined globals -- from the module. -- Setting a global not defined in the module results in "setting non-module global variable" warning. -- Extracts sets of defined, exported and used globals from a file report. local function get_defined_and_used_globals(file_report, opts) local defined, globally_defined, used = {}, {}, {} for _, warning in ipairs(file_report) do if warning.code:match("11.") then if warning.code == "111" then if (opts.inline and warning.definition) or core_utils.is_definition(opts, warning) then if (opts.inline and warning.in_module) or opts.module then defined[warning.name] = true else globally_defined[warning.name] = true end end else used[warning.name] = true end end end return defined, globally_defined, used end -- Returns {globally_defined = globally_defined, globally_used = globally_used, locally_defined = locally_defined}, -- where `globally_defined` is set of globals defined across all files except modules, -- where `globally_used` is set of globals defined across all files except modules, -- where `locally_defined` is an array of sets of globals defined per file. local function get_implicit_defs_info(report, opts) local info = { globally_defined = {}, globally_used = {}, locally_defined = {} } for i, file_report in ipairs(report) do local defined, globally_defined, used = get_defined_and_used_globals(file_report, opts[i]) utils.update(info.globally_defined, globally_defined) utils.update(info.globally_used, used) info.locally_defined[i] = defined end return info end -- Returns file report clear of implicit definitions. local function filter_implicit_defs_file(file_report, opts, globally_defined, globally_used, locally_defined) local res = {} for _, warning in ipairs(file_report) do if warning.code:match("11.") then if warning.code == "111" then if (opts.inline and warning.in_module) or opts.module then if not locally_defined[warning.name] then warning.module = true table.insert(res, warning) end else if (opts.inline and warning.definition) or core_utils.is_definition(opts, warning) then if not globally_used[warning.name] then warning.code = "131" warning.top = nil table.insert(res, warning) end else if not globally_defined[warning.name] then table.insert(res, warning) end end end else if not globally_defined[warning.name] and not locally_defined[warning.name] then table.insert(res, warning) end end else table.insert(res, warning) end end return res end -- Returns report clear of implicit definitions. local function filter_implicit_defs(report, opts) local res = {} local info = get_implicit_defs_info(report, opts) for i, file_report in ipairs(report) do if not file_report.fatal then res[i] = filter_implicit_defs_file(file_report, opts[i], info.globally_defined, info.globally_used, info.locally_defined[i]) else res[i] = file_report end end return res end -- Returns two optional booleans indicating if warning matches pattern by code and name. local function match(warning, pattern) local matches_code, matches_name local code_pattern, name_pattern = pattern[1], pattern[2] if code_pattern then matches_code = utils.pmatch(warning.code, code_pattern) end if name_pattern then if warning.code:match("5..") then -- Statement-related warnings can't match by name. matches_name = false else matches_name = utils.pmatch(warning.name, name_pattern) end end return matches_code, matches_name end local function is_enabled(rules, warning) -- A warning is enabled when its code and name are enabled. local enabled_code, enabled_name = false, false for _, rule in ipairs(rules) do local matches_one = false for _, pattern in ipairs(rule[1]) do local matches_code, matches_name = match(warning, pattern) -- If a factor is enabled, warning can't be disable by it. if enabled_code then matches_code = rule[2] ~= "disable" end if enabled_name then matches_code = rule[2] ~= "disable" end if (matches_code and matches_name ~= false) or (matches_name and matches_code ~= false) then matches_one = true end if rule[2] == "enable" then if matches_code then enabled_code = true end if matches_name then enabled_name = true end if enabled_code and enabled_name then -- Enable as matching to some `enable` pattern by code and to other by name. return true end elseif rule[2] == "disable" then if matches_one then -- Disable as matching to `disable` pattern. return false end end end if rule[2] == "only" and not matches_one then -- Disable as not matching to any of `only` patterns. return false end end -- Enable by default. return true end function filter.filters(opts, warning) if warning.code:match("[234]..") and warning.name == "_" then return true end if warning.code:match("11.") and not warning.module and opts.globals[warning.name] then return true end if warning.secondary and not opts.unused_secondaries then return true end if warning.self and not opts.self then return true end return not is_enabled(opts.rules, warning) end local function filter_file_report(report, opts) local res = {} for _, event in ipairs(report) do if ((opts.inline and event.read_only) or event.code:match("11[12]") and not event.module and opts.read_globals[event.name]) and not ( (opts.inline and event.global) or (opts.globals[event.name] and not opts.read_globals[event.name])) then event.code = "12" .. event.code:sub(3, 3) end if event.code == "011" or (event.code:match("02.") and opts.inline) or (event.code:sub(1, 1) ~= "0" and (not event.filtered and not event["filtered_" .. event.code] or not opts.inline) and not filter.filters(opts, event)) then table.insert(res, event) end end return res end -- Assumes `opts` are normalized. local function filter_report(report, opts) local res = {} for i, file_report in ipairs(report) do if not file_report.fatal then res[i] = filter_file_report(file_report, opts[i]) else res[i] = file_report end end return res end -- Removes warnings from report that do not match options. -- `opts[i]`, if present, is used as options when processing `report[i]` -- together with options in its array part. function filter.filter(report, opts) opts = get_normalized_opts(report, opts) report = filter_implicit_defs(report, opts) return filter_report(report, opts) end return filter luacheck-0.13.0/src/luacheck/stds.lua0000644000175000017500000001024212642521554016437 0ustar vsevavsevalocal utils = require "luacheck.utils" local stds = {} stds.busted = { "describe", "insulate", "expose", "it", "pending", "before_each", "after_each", "lazy_setup", "lazy_teardown", "strict_setup", "strict_teardown", "setup", "teardown", "context", "spec", "test", "assert", "spy", "mock", "stub", "finally"} stds.lua51 = { _G = true, package = true, "_VERSION", "arg", "assert", "collectgarbage", "coroutine", "debug", "dofile", "error", "gcinfo", "getfenv", "getmetatable", "io", "ipairs", "load", "loadfile", "loadstring", "math", "module", "newproxy", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawset", "require", "select", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "xpcall"} stds.lua52 = { _ENV = true, _G = true, package = true, "_VERSION", "arg", "assert", "bit32", "collectgarbage", "coroutine", "debug", "dofile", "error", "getmetatable", "io", "ipairs", "load", "loadfile", "math", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "string", "table", "tonumber", "tostring", "type", "xpcall"} stds.lua52c = { _ENV = true, _G = true, package = true, "_VERSION", "arg", "assert", "bit32", "collectgarbage", "coroutine", "debug", "dofile", "error", "getmetatable", "io", "ipairs", "load", "loadfile", "loadstring", "math", "module", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "xpcall"} stds.lua53 = { _ENV = true, _G = true, package = true, "_VERSION", "arg", "assert", "collectgarbage", "coroutine", "debug", "dofile", "error", "getmetatable", "io", "ipairs", "load", "loadfile", "math", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "string", "table", "tonumber", "tostring", "type", "utf8", "xpcall"} stds.lua53c = { _ENV = true, _G = true, package = true, "_VERSION", "arg", "assert", "bit32", "collectgarbage", "coroutine", "debug", "dofile", "error", "getmetatable", "io", "ipairs", "load", "loadfile", "math", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "string", "table", "tonumber", "tostring", "type", "utf8", "xpcall"} stds.luajit = { _G = true, package = true, "_VERSION", "arg", "assert", "bit", "collectgarbage", "coroutine", "debug", "dofile", "error", "gcinfo", "getfenv", "getmetatable", "io", "ipairs", "jit", "load", "loadfile", "loadstring", "math", "module", "newproxy", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawset", "require", "select", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "xpcall"} stds.ngx_lua = { _G = true, package = true, "_VERSION", "arg", "assert", "bit", "collectgarbage", "coroutine", "debug", "dofile", "error", "gcinfo", "getfenv", "getmetatable", "io", "ipairs", "jit", "load", "loadfile", "loadstring", "math", "module", "newproxy", "ndk", "ngx", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawset", "require", "select", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "xpcall"} local min = {_G = true, package = true} local std_sets = {} for name, std in pairs(stds) do std_sets[name] = utils.array_to_set(std) end for global in pairs(std_sets.lua51) do if std_sets.lua52[global] and std_sets.lua53[global] and std_sets.luajit[global] and std_sets.ngx_lua[global] then table.insert(min, global) end end stds.min = min stds.max = utils.concat_arrays {stds.lua51, stds.lua52, stds.lua53, stds.luajit} stds.max._G = true stds.max._ENV = true stds.max.package = true stds._G = {} for global in pairs(_G) do if global == "_G" or global == "package" then stds._G[global] = true else table.insert(stds._G, global) end end local function has_env() local _ENV = {} -- luacheck: ignore return not _G end if has_env() then stds._G._ENV = true end stds.none = {} return stds luacheck-0.13.0/src/luacheck/inline_options.lua0000644000175000017500000002506312642521554020522 0ustar vsevavsevalocal options = require "luacheck.options" local filter = require "luacheck.filter" local core_utils = require "luacheck.core_utils" local utils = require "luacheck.utils" -- Inline option is a comment starting with "luacheck:". -- Body can be "push", "pop" or comma delimited options, where option -- is option name plus space delimited arguments. -- "push" can also be immediately followed by options. -- If there is code on line with inline option, it only affects that line; -- otherwise, it affects everything till the end of current closure. -- Option scope can also be regulated using "push" and "pop" options: -- -- luacheck: push ignore foo -- foo() -- Ignored. -- -- luacheck: pop -- foo() -- Not ignored. local function add_closure_boundaries(ast, events) if ast.tag == "Function" then table.insert(events, {push = true, closure = true, line = ast.location.line, column = ast.location.column}) table.insert(events, {pop = true, closure = true, line = ast.end_location.line, column = ast.end_location.column}) else for _, node in ipairs(ast) do if type(node) == "table" then add_closure_boundaries(node, events) end end end end -- Parses inline option body, returns options or nil. local function get_options(body) local opts = {} for _, name_and_args in ipairs(utils.split(body, ",")) do local args = utils.split(name_and_args) local name = table.remove(args, 1) if not name then return end if name == "std" then if #args ~= 1 or not options.split_std(args[1]) then return end opts.std = args[1] elseif name == "ignore" and #args == 0 then opts.ignore = {".*/.*"} else local flag = true if name == "no" then flag = false name = table.remove(args, 1) end while true do if options.variadic_inline_options[name] then if flag then opts[name] = args break else -- Array option with 'no' prefix is invalid. return end elseif #args == 0 then if options.nullary_inline_options[name] then opts[name] = flag break else -- Consumed all arguments but didn't find a valid option name. return end else -- Join name with next argument, name = name.."_"..table.remove(args, 1) end end end end return opts end -- Returns whether option is valid. local function add_inline_option(events, per_line_opts, body, location, end_column, is_code_line) body = utils.strip(body) local after_push = body:match("^push%s+(.*)") if after_push then body = "push" end if body == "push" or body == "pop" then table.insert(events, {code = 1, [body] = true, line = location.line, column = location.column, end_column = end_column}) if after_push then body = after_push else return true end end local opts = get_options(body) if not opts then return false end if is_code_line and not after_push then if not per_line_opts[location.line] then per_line_opts[location.line] = {} end table.insert(per_line_opts[location.line], opts) else table.insert(events, {code = 2, options = opts, line = location.line, column = location.column, end_column = end_column}) end return true end -- Returns map of per line options and array of invalid comments. local function add_inline_options(events, comments, code_lines) local per_line_opts = {} local invalid_comments = {} for _, comment in ipairs(comments) do local contents = utils.strip(comment.contents) local body = utils.after(contents, "^luacheck:") if body then if not add_inline_option(events, per_line_opts, body, comment.location, comment.end_column, code_lines[comment.location.line]) then table.insert(invalid_comments, comment) end end end return per_line_opts, invalid_comments end local function alert_code(warning, code) local new_warning = utils.update({}, warning) new_warning.code = code return new_warning end local function apply_possible_filtering(opts, warning, code) if filter.filters(opts, code and alert_code(warning, code) or warning) then warning["filtered_" .. (code or warning.code)] = true end end local function apply_inline_options(option_stack, per_line_opts, warnings) if not option_stack.top.normalized then option_stack.top.normalize = options.normalize(option_stack) end local normalized_options = option_stack.top.normalize for _, warning in ipairs(warnings) do local opts = normalized_options if per_line_opts[warning.line] then opts = options.normalize(utils.concat_arrays({option_stack, per_line_opts[warning.line]})) end if warning.code:match("1..") then apply_possible_filtering(opts, warning) if warning.code ~= "113" then warning.read_only = opts.read_globals[warning.name] warning.global = opts.globals[warning.name] and not warning.read_only or nil if warning.code == "111" then if opts.module then warning.in_module = true warning.filtered_111 = nil end if core_utils.is_definition(opts, warning) then warning.definition = true end apply_possible_filtering(opts, warning, "121") apply_possible_filtering(opts, warning, "131") else apply_possible_filtering(opts, warning, "122") end end elseif filter.filters(opts, warning) then warning.filtered = true end end end -- Mutates shape of warnings in events according to inline options. -- Warnings which are simply filtered are marked with .filtered. -- Returns arrays of unpaired push events and unpaired pop events. local function handle_events(events, per_line_opts) local unpaired_pushes, unpaired_pops = {}, {} local unfiltered_warnings = {} local option_stack = utils.Stack() local boundaries = utils.Stack() option_stack:push({std = "none"}) -- Go through all events. for _, event in ipairs(events) do if type(event.code) == "string" then -- It's a warning, put it into list of not handled warnings. table.insert(unfiltered_warnings, event) elseif event.options then if #unfiltered_warnings ~= 0 then -- There are new options added and there were not handled warnings. -- Handle them using old option stack. apply_inline_options(option_stack, per_line_opts, unfiltered_warnings) unfiltered_warnings = {} end option_stack:push(event.options) elseif event.push then -- New boundary. Save size of the option stack to rollback later -- when boundary is popped. event.last_option_index = option_stack.size boundaries:push(event) elseif event.pop then if boundaries.size == 0 or (boundaries.top.closure and not event.closure) then -- Unpaired pop boundary, do nothing. table.insert(unpaired_pops, event) else if event.closure then -- There could be unpaired push boundaries, pop them. while not boundaries.top.closure do table.insert(unpaired_pushes, boundaries:pop()) end end -- Pop closure boundary. local new_last_option_index = boundaries:pop().last_option_index if new_last_option_index ~= option_stack.size and #unfiltered_warnings ~= 0 then -- Some options are going to be popped, handle not handled warnings. apply_inline_options(option_stack, per_line_opts, unfiltered_warnings) unfiltered_warnings = {} end while new_last_option_index ~= option_stack.size do option_stack:pop() end end end end if #unfiltered_warnings ~= 0 then apply_inline_options(option_stack, per_line_opts, unfiltered_warnings) end return unpaired_pushes, unpaired_pops end -- Filteres warnings using inline options, adds invalid comments. -- Warnings which are altered in shape: -- .filtered is added to warnings filtered by inline options; -- .filtered_ is added to warnings that would be filtered by inline options if their code was -- (111 can change to 121 and 131, 112 can change to 122); -- .definition is added to global set warnings (111) that are implicit definitions due to inline options; -- .in_module is added to 111 warnings that are in module due to inline options. -- .read_only is added to 111 and 112 warnings related to read only globals. -- .global is added to 111 and 112 related to regular globals. -- Invalid comments have same shape as warnings, with codes: -- 021 - syntactically invalid comment; -- 022 - unpaired push comment; -- 023 - unpaired pop comment. local function handle_inline_options(ast, comments, code_lines, warnings) -- Create array of all events sorted by location. -- This includes inline options, warnings and implicit push/pop operations corresponding to closure starts/ends. local events = utils.update({}, warnings) -- Add implicit push/pop around main chunk. table.insert(events, {push = true, closure = true, line = -1, column = 0}) table.insert(events, {pop = true, closure = true, line = math.huge, column = 0}) add_closure_boundaries(ast, events) local per_line_opts, invalid_comments = add_inline_options(events, comments, code_lines) core_utils.sort_by_location(events) local unpaired_pushes, unpaired_pops = handle_events(events, per_line_opts) for _, comment in ipairs(invalid_comments) do table.insert(warnings, {code = "021", line = comment.location.line, column = comment.location.column, end_column = comment.end_column}) end for _, event in ipairs(unpaired_pushes) do table.insert(warnings, {code = "022", line = event.line, column = event.column, end_column = event.end_column}) end for _, event in ipairs(unpaired_pops) do table.insert(warnings, {code = "023", line = event.line, column = event.column, end_column = event.end_column}) end return warnings end return handle_inline_options luacheck-0.13.0/src/luacheck/globbing.lua0000644000175000017500000000644512642521554017257 0ustar vsevavsevalocal fs = require "luacheck.fs" local utils = require "luacheck.utils" -- Only ?, *, ** and simple character classes (with ranges and negation) are supported. -- Hidden files are not treated specially. Special characters can't be escaped. local globbing = {} local cur_dir = fs.current_dir() local function is_regular_path(glob) return not glob:find("[*?%[]") end local function get_parts(path) local parts = {} for part in path:gmatch("[^"..utils.dir_sep.."]+") do table.insert(parts, part) end return parts end local function glob_pattern_escaper(c) return ((c == "*" or c == "?") and "." or "%")..c end local function glob_range_escaper(c) return c == "-" and c or ("%"..c) end local function glob_part_to_pattern(glob_part) local buffer = {"^"} local i = 1 while i <= #glob_part do local bracketless bracketless, i = glob_part:match("([^%[]*)()", i) table.insert(buffer, (bracketless:gsub("%p", glob_pattern_escaper))) if glob_part:sub(i, i) == "[" then table.insert(buffer, "[") i = i + 1 local first_char = glob_part:sub(i, i) if first_char == "!" then table.insert(buffer, "^") i = i + 1 elseif first_char == "]" then table.insert(buffer, "%]") i = i + 1 end bracketless, i = glob_part:match("([^%]]*)()", i) if bracketless:sub(1, 1) == "-" then table.insert(buffer, "%-") bracketless = bracketless:sub(2) end local last_dash = "" if bracketless:sub(-1) == "-" then last_dash = "-" bracketless = bracketless:sub(1, -2) end table.insert(buffer, (bracketless:gsub("%p", glob_range_escaper))) table.insert(buffer, last_dash.."]") i = i + 1 end end table.insert(buffer, "$") return table.concat(buffer) end local function part_match(glob_part, path_part) return utils.pmatch(path_part, glob_part_to_pattern(glob_part)) end local function parts_match(glob_parts, glob_i, path_parts, path_i) local glob_part = glob_parts[glob_i] if not glob_part then -- Reached glob end, path matches the glob or its subdirectory. -- E.g. path "foo/bar/baz/src.lua" matches glob "foo/*/baz". return true end if glob_part == "**" then -- "**" can consume any number of path parts. for i = path_i, #path_parts + 1 do if parts_match(glob_parts, glob_i + 1, path_parts, i) then return true end end return false end local path_part = path_parts[path_i] return path_part and part_match(glob_part, path_part) and parts_match(glob_parts, glob_i + 1, path_parts, path_i + 1) end -- Checks if a path matches a globbing pattern. function globbing.match(glob, path) glob = fs.normalize(fs.join(cur_dir, glob)) path = fs.normalize(fs.join(cur_dir, path)) if is_regular_path(glob) then return fs.is_subpath(glob, path) end local glob_base, path_base glob_base, glob = fs.split_base(glob) path_base, path = fs.split_base(path) if glob_base ~= path_base then return false end local glob_parts = get_parts(glob) local path_parts = get_parts(path) return parts_match(glob_parts, 1, path_parts, 1) end return globbing luacheck-0.13.0/src/luacheck/expand_rockspec.lua0000644000175000017500000000177512642521554020645 0ustar vsevavsevalocal utils = require "luacheck.utils" local function extract_lua_files(rockspec) local res = {} local build = rockspec.build local function scan(t) for _, file in pairs(t) do if type(file) == "string" and file:sub(-#".lua") == ".lua" then table.insert(res, file) end end end if build.type == "builtin" then scan(build.modules) end if build.install then if build.install.lua then scan(build.install.lua) end if build.install.bin then scan(build.install.bin) end end table.sort(res) return res end -- Receives a name of a rockspec, returns list of related .lua files or nil and "syntax" or "error". local function expand_rockspec(file) local rockspec, err = utils.load_config(file) if not rockspec then return nil, err end local ok, files = pcall(extract_lua_files, rockspec) if not ok then return nil, "syntax" end return files end return expand_rockspec luacheck-0.13.0/src/luacheck/reachability.lua0000644000175000017500000000244512642521554020130 0ustar vsevavsevalocal core_utils = require "luacheck.core_utils" local reachability local function noop_callback() end local function reachability_callback(_, _, item, chstate, nested) if not item then return true end if not nested and item.lines then for _, subline in ipairs(item.lines) do reachability(chstate, subline, true) end end if item.accesses then for var, accessing_nodes in pairs(item.accesses) do local possible_values = item.used_values[var] if not var.empty and (#possible_values == 1) and possible_values[1].empty then for _, accessing_node in ipairs(accessing_nodes) do chstate:warn_uninit(accessing_node) end end end end 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) 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) end end end end return reachability luacheck-0.13.0/src/luacheck/main.lua0000644000175000017500000004150612642521554016415 0ustar vsevavsevalocal luacheck = require "luacheck" local argparse = require "luacheck.argparse" local config = require "luacheck.config" local options = require "luacheck.options" local expand_rockspec = require "luacheck.expand_rockspec" local multithreading = require "luacheck.multithreading" local cache = require "luacheck.cache" local format = require "luacheck.format" local version = require "luacheck.version" local fs = require "luacheck.fs" local globbing = require "luacheck.globbing" local utils = require "luacheck.utils" local function critical(msg) io.stderr:write("Critical error: "..msg.."\n") os.exit(3) end local function global_error_handler(err) if type(err) == "table" and err.pattern then critical("Invalid pattern '" .. err.pattern .. "'") else critical(debug.traceback( ("Luacheck %s bug (please report at github.com/mpeterv/luacheck/issues):\n%s"):format(luacheck._VERSION, err), 2)) end end local function main() local default_cache_path = ".luacheckcache" local function get_parser() local parser = argparse("luacheck", "luacheck " .. luacheck._VERSION .. ", a simple static analyzer for Lua.", [[ Links: Luacheck on GitHub: https://github.com/mpeterv/luacheck Luacheck documentation: http://luacheck.readthedocs.org]]) parser:argument "files" :description (fs.has_lfs and [[List of files, directories and rockspecs to check. Pass "-" to check stdin.]] or [[List of files and rockspecs to check. Pass "-" to check stdin.]]) :args "+" :argname "" parser:flag("-g --no-global", [[Filter out warnings related to global variables. Equivalent to --ignore 1.]]):target("global"):action("store_false") parser:flag("-u --no-unused", [[Filter out warnings related to unused variables and values. Equivalent to --ignore [23].]]):target("unused"):action("store_false") parser:flag("-r --no-redefined", [[Filter out warnings related to redefined variables. Equivalent to --ignore 4.]]):target("redefined"):action("store_false") parser:flag("-a --no-unused-args", [[Filter out warnings related to unused arguments and loop variables. Equivalent to --ignore 21[23].]]):target("unused_args"):action("store_false") parser:flag("-s --no-unused-secondaries", [[Filter out warnings related to unused variables set together with used ones.]]):target("unused_secondaries"):action("store_false") parser:flag("--no-self", "Filter out warnings related to implicit self argument."):target("self"):action("store_false") parser:option("--std", [[Set standard globals. can be one of: _G (default) - globals of the current Lua interpreter; lua51 - globals of Lua 5.1; lua52 - globals of Lua 5.2; lua52c - globals of Lua 5.2 with LUA_COMPAT_ALL; lua53 - globals of Lua 5.3; lua53c - globals of Lua 5.3 with LUA_COMPAT_5_2; luajit - globals of LuaJIT 2.0; ngx_lua - globals of Openresty lua-nginx-module with LuaJIT 2.0; min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0; max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0; busted - globals added by Busted 2.0; none - no standard globals. Sets can be combined using "+".]]) parser:option("--globals", "Add custom globals on top of standard ones.") :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--read-globals", "Add read-only globals.") :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--new-globals", [[Set custom globals. Removes custom globals added previously.]]) :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--new-read-globals", [[Set read-only globals. Removes read-only globals added previously.]]) :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:flag("-c --compat", "Equivalent to --std max.") parser:flag("-d --allow-defined", "Allow defining globals implicitly by setting them.") parser:flag("-t --allow-defined-top", [[Allow defining globals implicitly by setting them in the top level scope.]]) parser:flag("-m --module", [[Limit visibility of implicitly defined globals to their files.]]) parser:option("--ignore -i", [[Filter out warnings matching these patterns. If a pattern contains slash, part before slash matches warning code and part after it matches name of related variable. Otherwise, if the pattern contains letters or underscore, it matches name of related variable. Otherwise, the pattern matches warning code.]]) :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:option("--enable -e", "Do not filter out warnings matching these patterns.") :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:option("--only -o", "Filter out warnings not matching these patterns.") :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:flag("--no-inline", "Disable inline options."):target("inline"):action("store_false") parser:mutex( parser:option("--config", "Path to configuration file. (default: "..config.default_path..")"), parser:flag("--no-config", "Do not look up configuration file.") ) parser:option("--filename", [[Use another filename in output and for selecting configuration overrides.]]) parser:option("--exclude-files", "Do not check files matching these globbing patterns.") :args "+" :count "*" :argname "" parser:option("--include-files", [[Do not check files not matching these globbing patterns.]]) :args "+" :count "*" :argname "" if fs.has_lfs then parser:mutex( parser:option("--cache", "Path to cache file.", default_cache_path) :defmode "arg", parser:flag("--no-cache", "Do not use cache.") ) end if multithreading.has_lanes then parser:option("-j --jobs", "Check files in parallel.") :convert(tonumber) end parser:option("--formatter" , [[Use custom formatter. must be a module name or one of: TAP - Test Anything Protocol formatter; JUnit - JUnit XML formatter; plain - simple warning-per-line formatter; default - standard formatter.]]) parser:flag("-q --quiet", [[Suppress output for files without warnings. -qq: Suppress output of warnings. -qqq: Only print total number of warnings and errors.]]) :count "0-3" parser:flag("--codes", "Show warning codes.") parser:flag("--ranges", "Show ranges of columns related to warnings.") parser:flag("--no-color", "Do not color output.") parser:flag("-v --version", "Show version info and exit.") :action(function() print(version.string) os.exit(0) end) return parser end local function match_any(globs, name) for _, glob in ipairs(globs) do if globbing.match(glob, name) then return true end end return false end local function is_included(args, name) return not match_any(args.exclude_files, name) and (#args.include_files == 0 or match_any(args.include_files, name)) end -- Expands folders, rockspecs, - -- Returns new array of filenames and table mapping indexes of bad rockspecs to error messages. -- Removes "./" in the beginnings of file names. -- Filters filenames using args.exclude_files and args.include_files. local function expand_files(args) -- If --include-files is used, do not focus on .lua files in directories. local dir_pattern = #args.include_files > 0 and "" or "%.lua$" local res, bad_rockspecs = {}, {} local function add(file) if type(file) == "string" then file = file:gsub("^./", "") end local name = args.filename or file if type(name) == "string" then if not is_included(args, name) then return false end end table.insert(res, file) return true end for _, file in ipairs(args.files) do if file == "-" then add(io.stdin) elseif fs.is_dir(file) then for _, nested_file in ipairs(fs.extract_files(file, dir_pattern)) do add(nested_file) end elseif file:sub(-#".rockspec") == ".rockspec" then local related_files, err = expand_rockspec(file) if related_files then for _, related_file in ipairs(related_files) do add(related_file) end else if add(file) then bad_rockspecs[#res] = err end end else add(file) end end return res, bad_rockspecs end local function validate_args(args, parser) if args.jobs and args.jobs < 1 then parser:error(" must be at least 1") end if args.std and not options.split_std(args.std) then parser:error(" must name a standard library") end end local function combine_conf_and_args_path_arrays(conf, args, option) local conf_opts = config.get_top_options(conf) if conf_opts[option] then for i, path in ipairs(conf_opts[option]) do conf_opts[option][i] = config.relative_path(conf, path) end table.insert(args[option], conf_opts[option]) end args[option] = utils.concat_arrays(args[option]) end -- Applies cli-specific options from config to args. local function combine_config_and_args(conf, args) local conf_opts = config.get_top_options(conf) if args.no_color then args.color = false else args.color = conf_opts.color ~= false end args.codes = args.codes or conf_opts.codes args.formatter = args.formatter or conf_opts.formatter or "default" if args.no_cache or not fs.has_lfs then args.cache = false elseif not args.cache then if type(conf_opts.cache) == "string" then args.cache = config.relative_path(conf, conf_opts.cache) else args.cache = conf_opts.cache end end if args.cache == true then args.cache = config.relative_path(conf, default_cache_path) end args.jobs = args.jobs or conf_opts.jobs combine_conf_and_args_path_arrays(conf, args, "exclude_files") combine_conf_and_args_path_arrays(conf, args, "include_files") end -- Returns sparse array of mtimes and map of filenames to cached reports. local function get_mtimes_and_cached_reports(cache_filename, files, bad_files) local cache_files = {} local cache_mtimes = {} local sparse_mtimes = {} for i, file in ipairs(files) do if not bad_files[i] and file ~= io.stdin then table.insert(cache_files, file) local mtime = fs.mtime(file) table.insert(cache_mtimes, mtime) sparse_mtimes[i] = mtime end end return sparse_mtimes, cache.load(cache_filename, cache_files, cache_mtimes) or critical( ("Couldn't load cache from %s: data corrupted"):format(cache_filename)) end -- Returns sparse array of sources of files that need to be checked, updates bad_files with files that had I/O issues. local function get_srcs_to_check(cached_reports, files, bad_files) local res = {} for i, file in ipairs(files) do if not bad_files[i] and not cached_reports[file] then local src = utils.read_file(file) if src then res[i] = src else bad_files[i] = "I/O" end end end return res end -- Returns sparse array of new reports. local function get_new_reports(files, srcs, jobs) local dense_srcs = {} local dense_to_sparse = {} for i in ipairs(files) do if srcs[i] then table.insert(dense_srcs, srcs[i]) dense_to_sparse[#dense_srcs] = i end end local map = jobs and multithreading.has_lanes and multithreading.pmap or utils.map local dense_res = map(luacheck.get_report, dense_srcs, jobs) local res = {} for i in ipairs(dense_srcs) do res[dense_to_sparse[i]] = dense_res[i] end return res end -- Updates cache with new_reports. Updates bad_files for which mtime is absent. local function update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports) local cache_files = {} local cache_mtimes = {} local cache_reports = {} for i, file in ipairs(files) do if srcs[i] and file ~= io.stdin then if not mtimes[i] then bad_files[i] = "I/O" else table.insert(cache_files, file) table.insert(cache_mtimes, mtimes[i]) table.insert(cache_reports, new_reports[i] or false) end end end return cache.update(cache_filename, cache_files, cache_mtimes, cache_reports) or critical( ("Couldn't save cache to %s: I/O error"):format(cache_filename)) end -- Returns array of reports for files. local function get_reports(cache_filename, files, bad_rockspecs, jobs) local bad_files = utils.update({}, bad_rockspecs) local mtimes local cached_reports if cache_filename then mtimes, cached_reports = get_mtimes_and_cached_reports(cache_filename, files, bad_files) else cached_reports = {} end local srcs = get_srcs_to_check(cached_reports, files, bad_files) local new_reports = get_new_reports(files, srcs, jobs) if cache_filename then update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports) end local res = {} for i, file in ipairs(files) do if bad_files[i] then res[i] = {fatal = bad_files[i]} else res[i] = cached_reports[file] or new_reports[i] end end return res end local function combine_config_and_options(conf, cli_opts, files) local res = {} for i, file in ipairs(files) do res[i] = config.get_options(conf, file) table.insert(res[i], cli_opts) end return res end local function substitute_filename(new_filename, files) if new_filename then for i = 1, #files do files[i] = new_filename end end end local function normalize_stdin_in_filenames(files) for i, file in ipairs(files) do if type(file) ~= "string" then files[i] = "stdin" end end end local builtin_formatters = utils.array_to_set({"TAP", "JUnit", "plain", "default"}) local function pformat(report, file_names, conf, args) if builtin_formatters[args.formatter] then return format.format(report, file_names, args) end local formatter = args.formatter local ok, output if type(formatter) == "string" then ok, formatter = config.relative_require(conf, formatter) if not ok then critical(("Couldn't load custom formatter '%s': %s"):format(args.formatter, formatter)) end end ok, output = pcall(formatter, report, file_names, args) if not ok then critical(("Couldn't run custom formatter '%s': %s"):format(tostring(args.formatter), output)) end return output end local parser = get_parser() local args = parser:parse() local conf if args.no_config then conf = config.empty_config else local err conf, err = config.load_config(args.config) if not conf then critical(err) end end -- Validate args only after loading config so that custom stds are already in place. validate_args(args, parser) combine_config_and_args(conf, args) local files, bad_rockspecs = expand_files(args) local reports = get_reports(args.cache, files, bad_rockspecs, args.jobs) substitute_filename(args.filename, files) local report = luacheck.process_reports(reports, combine_config_and_options(conf, args, files)) normalize_stdin_in_filenames(files) local output = pformat(report, files, conf, args) if #output > 0 and output:sub(-1) ~= "\n" then output = output .. "\n" end io.stdout:write(output) local exit_code if report.fatals > 0 then exit_code = 2 elseif report.warnings > 0 or report.errors > 0 then exit_code = 1 else exit_code = 0 end os.exit(exit_code) end xpcall(main, global_error_handler) luacheck-0.13.0/src/luacheck/format.lua0000644000175000017500000002407712642521554016765 0ustar vsevavsevalocal utils = require "luacheck.utils" local format = {} local color_support = not utils.is_windows or os.getenv("ANSICON") local message_formats = { ["011"] = function(w) return (w.msg:gsub("%%", "%%%%")) end, ["021"] = "invalid inline option", ["022"] = "unpaired push directive", ["023"] = "unpaired pop directive", ["111"] = function(w) if w.module then return "setting non-module global variable %s" else return "setting non-standard global variable %s" end end, ["112"] = "mutating non-standard global variable %s", ["113"] = "accessing undefined variable %s", ["121"] = "setting read-only global variable %s", ["122"] = "mutating read-only global variable %s", ["131"] = "unused global variable %s", ["211"] = function(w) if w.func then return "unused function %s" else return "unused variable %s" end end, ["212"] = function(w) if w.name == "..." then return "unused variable length argument" else return "unused argument %s" end end, ["213"] = "unused loop variable %s", ["221"] = "variable %s is never set", ["231"] = "variable %s is never accessed", ["232"] = "argument %s is never accessed", ["233"] = "loop variable %s is never accessed", ["311"] = "value assigned to variable %s is unused", ["312"] = "value of argument %s is unused", ["313"] = "value of loop variable %s is unused", ["321"] = "accessing uninitialized variable %s", ["411"] = "variable %s was previously defined on line %s", ["412"] = "variable %s was previously defined as an argument on line %s", ["413"] = "variable %s was previously defined as a loop variable on line %s", ["421"] = "shadowing definition of variable %s on line %s", ["422"] = "shadowing definition of argument %s on line %s", ["423"] = "shadowing definition of loop variable %s on line %s", ["431"] = "shadowing upvalue %s on line %s", ["432"] = "shadowing upvalue argument %s on line %s", ["433"] = "shadowing upvalue loop variable %s on line %s", ["511"] = "unreachable code", ["512"] = "loop is executed at most once", ["521"] = "unused label %s", ["531"] = "left-hand side of assignment is too short", ["532"] = "left-hand side of assignment is too long", ["541"] = "empty do..end block", ["542"] = "empty if branch", ["551"] = "empty statement" } local function get_message_format(warning) local message_format = message_formats[warning.code] if type(message_format) == "function" then return message_format(warning) else return message_format end end local function plural(number) return (number == 1) and "" or "s" end local color_codes = { reset = 0, bright = 1, red = 31, green = 32 } local function encode_color(c) return "\27[" .. tostring(color_codes[c]) .. "m" end local function colorize(str, ...) str = str .. encode_color("reset") for _, color in ipairs({...}) do str = encode_color(color) .. str end return encode_color("reset") .. str end local function format_color(str, color, ...) return color and colorize(str, ...) or str end local function format_name(name, color) return color and colorize(name, "bright") or ("'" .. name .. "'") end local function format_number(number, color) return format_color(tostring(number), color, "bright", (number > 0) and "red" or "reset") end local function capitalize(str) return str:gsub("^.", string.upper) end local function fatal_type(file_report) return capitalize(file_report.fatal) .. " error" end local function count_warnings_errors(events) local warnings, errors = 0, 0 for _, event in ipairs(events) do if event.code:sub(1, 1) == "0" then errors = errors + 1 else warnings = warnings + 1 end end return warnings, errors end local function format_file_report_header(report, file_name, opts) local label = "Checking " .. file_name local status if report.fatal then status = format_color(fatal_type(report), opts.color, "bright") elseif #report == 0 then status = format_color("OK", opts.color, "bright", "green") else local warnings, errors = count_warnings_errors(report) if warnings > 0 then status = format_color(tostring(warnings).." warning"..plural(warnings), opts.color, "bright", "red") end if errors > 0 then status = status and (status.." / ") or "" status = status..(format_color(tostring(errors).." error"..plural(errors), opts.color, "bright")) end end return label .. (" "):rep(math.max(50 - #label, 1)) .. status end local function format_location(file, location, opts) local res = ("%s:%d:%d"):format(file, location.line, location.column) if opts.ranges then res = ("%s-%d"):format(res, location.end_column) end return res end local function event_code(event) return (event.code:sub(1, 1) == "0" and "E" or "W")..event.code end local function format_message(event, color) return get_message_format(event):format(event.name and format_name(event.name, color), event.prev_line) end -- Returns formatted message for an issue, without color. function format.get_message(event) return format_message(event) end local function format_event(file_name, event, opts) local message = format_message(event, opts.color) if opts.codes then message = ("(%s) %s"):format(event_code(event), message) end return format_location(file_name, event, opts) .. ": " .. message end local function format_file_report(report, file_name, opts) local buf = {format_file_report_header(report, file_name, opts)} if #report > 0 then table.insert(buf, "") for _, event in ipairs(report) do table.insert(buf, " " .. format_event(file_name, event, opts)) end table.insert(buf, "") end return table.concat(buf, "\n") end local formatters = {} function formatters.default(report, file_names, opts) local buf = {} if opts.quiet <= 2 then for i, file_report in ipairs(report) do if opts.quiet == 0 or file_report.fatal or #file_report > 0 then table.insert(buf, (opts.quiet == 2 and format_file_report_header or format_file_report) ( file_report, file_names[i], opts)) end end if #buf > 0 and buf[#buf]:sub(-1) ~= "\n" then table.insert(buf, "") end end local total = ("Total: %s warning%s / %s error%s in %d file%s"):format( format_number(report.warnings, opts.color), plural(report.warnings), format_number(report.errors, opts.color), plural(report.errors), #report - report.fatals, plural(#report - report.fatals)) if report.fatals > 0 then total = total..(", couldn't check %s file%s"):format( report.fatals, plural(report.fatals)) end table.insert(buf, total) return table.concat(buf, "\n") end function formatters.TAP(report, file_names, opts) opts.color = false local buf = {} for i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ("not ok %d %s: %s"):format(#buf + 1, file_names[i], fatal_type(file_report))) elseif #file_report == 0 then table.insert(buf, ("ok %d %s"):format(#buf + 1, file_names[i])) else for _, warning in ipairs(file_report) do table.insert(buf, ("not ok %d %s"):format(#buf + 1, format_event(file_names[i], warning, opts))) end end end table.insert(buf, 1, "1.." .. tostring(#buf)) return table.concat(buf, "\n") end function formatters.JUnit(report, file_names) -- JUnit formatter doesn't support any options. local opts = {} local buf = {[[]]} local num_testcases = 0 for _, file_report in ipairs(report) do if file_report.fatal or #file_report == 0 then num_testcases = num_testcases + 1 else num_testcases = num_testcases + #file_report end end table.insert(buf, ([[]]):format(num_testcases)) for file_i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ([[ ]]):format(file_names[file_i], file_names[file_i])) table.insert(buf, ([[ ]]):format(fatal_type(file_report))) table.insert(buf, [[ ]]) elseif #file_report == 0 then table.insert(buf, ([[ ]]):format(file_names[file_i], file_names[file_i])) else for event_i, event in ipairs(file_report) do table.insert(buf, ([[ ]]):format(file_names[file_i], event_i, file_names[file_i])) table.insert(buf, ([[ ]]):format( event_code(event), format_event(file_names[file_i], event, opts))) table.insert(buf, [[ ]]) end end end table.insert(buf, [[]]) return table.concat(buf, "\n") end function formatters.plain(report, file_names, opts) opts.color = false local buf = {} for i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ("%s: %s"):format(file_names[i], fatal_type(file_report))) else for _, event in ipairs(file_report) do table.insert(buf, format_event(file_names[i], event, opts)) end end end return table.concat(buf, "\n") end --- Formats a report. -- Recognized options: -- `options.formatter`: name of used formatter. Default: "default". -- `options.quiet`: integer in range 0-3. See CLI. Default: 0. -- `options.color`: should use ansicolors? Default: true. -- `options.codes`: should output warning codes? Default: false. -- `options.ranges`: should output token end column? Default: false. function format.format(report, file_names, options) return formatters[options.formatter or "default"](report, file_names, { quiet = options.quiet or 0, color = (options.color ~= false) and color_support, codes = options.codes, ranges = options.ranges }) end return format luacheck-0.13.0/src/luacheck/lexer.lua0000644000175000017500000004454712642521554016620 0ustar vsevavsevalocal utils = require "luacheck.utils" -- Lexer should support syntax of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT(64bit and complex cdata literals). local lexer = {} local sbyte = string.byte local ssub = string.sub local schar = string.char local sreverse = string.reverse local tconcat = table.concat local mfloor = math.floor -- No point in inlining these, fetching a constant ~= fetching a local. local BYTE_0, BYTE_9, BYTE_f, BYTE_F = sbyte("0"), sbyte("9"), sbyte("f"), sbyte("F") local BYTE_x, BYTE_X, BYTE_i, BYTE_I = sbyte("x"), sbyte("X"), sbyte("i"), sbyte("I") local BYTE_l, BYTE_L, BYTE_u, BYTE_U = sbyte("l"), sbyte("L"), sbyte("u"), sbyte("U") local BYTE_e, BYTE_E, BYTE_p, BYTE_P = sbyte("e"), sbyte("E"), sbyte("p"), sbyte("P") local BYTE_a, BYTE_z, BYTE_A, BYTE_Z = sbyte("a"), sbyte("z"), sbyte("A"), sbyte("Z") local BYTE_DOT, BYTE_COLON = sbyte("."), sbyte(":") local BYTE_OBRACK, BYTE_CBRACK = sbyte("["), sbyte("]") local BYTE_OBRACE, BYTE_CBRACE = sbyte("{"), sbyte("}") local BYTE_QUOTE, BYTE_DQUOTE = sbyte("'"), sbyte('"') local BYTE_PLUS, BYTE_DASH, BYTE_LDASH = sbyte("+"), sbyte("-"), sbyte("_") local BYTE_SLASH, BYTE_BSLASH = sbyte("/"), sbyte("\\") local BYTE_EQ, BYTE_NE = sbyte("="), sbyte("~") local BYTE_LT, BYTE_GT = sbyte("<"), sbyte(">") local BYTE_LF, BYTE_CR = sbyte("\n"), sbyte("\r") local BYTE_SPACE, BYTE_FF, BYTE_TAB, BYTE_VTAB = sbyte(" "), sbyte("\f"), sbyte("\t"), sbyte("\v") local function to_hex(b) if BYTE_0 <= b and b <= BYTE_9 then return b-BYTE_0 elseif BYTE_a <= b and b <= BYTE_f then return 10+b-BYTE_a elseif BYTE_A <= b and b <= BYTE_F then return 10+b-BYTE_A else return nil end end local function to_dec(b) if BYTE_0 <= b and b <= BYTE_9 then return b-BYTE_0 else return nil end end local function to_utf(codepoint) if codepoint < 0x80 then -- ASCII? return schar(codepoint) end local buf = {} local mfb = 0x3F repeat buf[#buf+1] = schar(codepoint % 0x40 + 0x80) codepoint = mfloor(codepoint / 0x40) mfb = mfloor(mfb / 2) until codepoint <= mfb buf[#buf+1] = schar(0xFE - mfb*2 + codepoint) return sreverse(tconcat(buf)) end local function is_alpha(b) return (BYTE_a <= b and b <= BYTE_z) or (BYTE_A <= b and b <= BYTE_Z) or b == BYTE_LDASH end local function is_newline(b) return (b == BYTE_LF) or (b == BYTE_CR) end local function is_space(b) return (b == BYTE_SPACE) or (b == BYTE_FF) or (b == BYTE_TAB) or (b == BYTE_VTAB) end local keywords = utils.array_to_set({ "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}) local simple_escapes = { [sbyte("a")] = sbyte("\a"), [sbyte("b")] = sbyte("\b"), [sbyte("f")] = sbyte("\f"), [sbyte("n")] = sbyte("\n"), [sbyte("r")] = sbyte("\r"), [sbyte("t")] = sbyte("\t"), [sbyte("v")] = sbyte("\v"), [BYTE_BSLASH] = BYTE_BSLASH, [BYTE_QUOTE] = BYTE_QUOTE, [BYTE_DQUOTE] = BYTE_DQUOTE } local function next_byte(state, inc) inc = inc or 1 state.offset = state.offset+inc return sbyte(state.src, state.offset) end -- Skipping helpers. -- Take the current character, skip something, return next character. local function skip_newline(state, newline) local b = next_byte(state) if b ~= newline and is_newline(b) then b = next_byte(state) end state.line = state.line+1 state.line_offset = state.offset return b end local function skip_till_newline(state, b) while not is_newline(b) and b ~= nil do b = next_byte(state) end return b end local function skip_space(state, b) while is_space(b) or is_newline(b) do if is_newline(b) then b = skip_newline(state, b) else b = next_byte(state) end end return b end -- Skips "[=*" or "]=*". Returns next character and number of "="s. local function skip_long_bracket(state) local start = state.offset local b = next_byte(state) while b == BYTE_EQ do b = next_byte(state) end return b, state.offset-start-1 end -- Token handlers. -- Called after the opening "[=*" has been skipped. -- Takes number of "=" in the opening bracket and token type(comment or string). local function lex_long_string(state, opening_long_bracket, token) local b = next_byte(state) if is_newline(b) then b = skip_newline(state, b) end local lines = {} local line_start = state.offset while true do if is_newline(b) then -- Add the finished line. lines[#lines+1] = ssub(state.src, line_start, state.offset-1) b = skip_newline(state, b) line_start = state.offset elseif b == BYTE_CBRACK then local long_bracket b, long_bracket = skip_long_bracket(state) if b == BYTE_CBRACK and long_bracket == opening_long_bracket then break end elseif b == nil then return nil, token == "string" and "unfinished long string" or "unfinished long comment" else b = next_byte(state) end end -- Add last line. lines[#lines+1] = ssub(state.src, line_start, state.offset-opening_long_bracket-2) next_byte(state) return token, tconcat(lines, "\n") end local function lex_short_string(state, quote) local b = next_byte(state) local chunks -- Buffer is only required when there are escape sequences. local chunk_start = state.offset while b ~= quote do if b == BYTE_BSLASH then -- Escape sequence. if not chunks then -- This is the first escape sequence, init buffer. chunks = {} end -- Put previous chunk into buffer. if chunk_start ~= state.offset then chunks[#chunks+1] = ssub(state.src, chunk_start, state.offset-1) end b = next_byte(state) -- The final string escape sequence evaluates to. local s local escape_byte = simple_escapes[b] if escape_byte then -- Is it a simple escape sequence? b = next_byte(state) s = schar(escape_byte) elseif is_newline(b) then b = skip_newline(state, b) s = "\n" elseif b == BYTE_x then -- Hexadecimal escape. b = next_byte(state) -- Skip "x". -- Exactly two hexadecimal digits. local c1, c2 if b then c1 = to_hex(b) end if not c1 then return nil, "invalid hexadecimal escape sequence", -2 end b = next_byte(state) if b then c2 = to_hex(b) end if not c2 then return nil, "invalid hexadecimal escape sequence", -3 end b = next_byte(state) s = schar(c1*16 + c2) elseif b == BYTE_u then b = next_byte(state) -- Skip "u". if b ~= BYTE_OBRACE then return nil, "invalid UTF-8 escape sequence", -2 end b = next_byte(state) -- Skip "{". local codepoint -- There should be at least one digit. if b then codepoint = to_hex(b) end if not codepoint then return nil, "invalid UTF-8 escape sequence", -3 end local hexdigits = 0 while true do b = next_byte(state) local hex if b then hex = to_hex(b) end if hex then hexdigits = hexdigits + 1 codepoint = codepoint*16 + hex if codepoint > 0x10FFFF then -- UTF-8 value too large. return nil, "invalid UTF-8 escape sequence", -hexdigits-3 end else break end end if b ~= BYTE_CBRACE then return nil, "invalid UTF-8 escape sequence", -hexdigits-4 end b = next_byte(state) -- Skip "}". s = to_utf(codepoint) elseif b == BYTE_z then -- Zap following span of spaces. b = skip_space(state, next_byte(state)) else -- Must be a decimal escape. local cb if b then cb = to_dec(b) end if not cb then return nil, "invalid escape sequence", -1 end -- Up to three decimal digits. b = next_byte(state) if b then local c2 = to_dec(b) if c2 then cb = 10*cb + c2 b = next_byte(state) if b then local c3 = to_dec(b) if c3 then cb = 10*cb + c3 if cb > 255 then return nil, "invalid decimal escape sequence", -3 end b = next_byte(state) end end end end s = schar(cb) end if s then chunks[#chunks+1] = s end -- Next chunk starts after escape sequence. chunk_start = state.offset elseif b == nil or is_newline(b) then return nil, "unfinished string" else b = next_byte(state) end end -- Offset now points at the closing quote. local string_value if chunks then -- Put last chunk into buffer. if chunk_start ~= state.offset then chunks[#chunks+1] = ssub(state.src, chunk_start, state.offset-1) end string_value = tconcat(chunks) else -- There were no escape sequences. string_value = ssub(state.src, chunk_start, state.offset-1) end next_byte(state) -- Skip the closing quote. return "string", string_value end -- Payload for a number is simply a substring. -- Luacheck is supposed to be forward-compatible with Lua 5.3 and LuaJIT syntax, so -- parsing it into actual number may be problematic. -- It is not needed currently anyway as Luacheck does not do static evaluation yet. local function lex_number(state, b) local start = state.offset local exp_lower, exp_upper = BYTE_e, BYTE_E local is_digit = to_dec local has_digits = false local is_float = false if b == BYTE_0 then b = next_byte(state) if b == BYTE_x or b == BYTE_X then exp_lower, exp_upper = BYTE_p, BYTE_P is_digit = to_hex b = next_byte(state) else has_digits = true end end while b ~= nil and is_digit(b) do b = next_byte(state) has_digits = true end if b == BYTE_DOT then -- Fractional part. is_float = true b = next_byte(state) -- Skip dot. while b ~= nil and is_digit(b) do b = next_byte(state) has_digits = true end end if b == exp_lower or b == exp_upper then -- Exponent part. is_float = true b = next_byte(state) -- Skip optional sign. if b == BYTE_PLUS or b == BYTE_DASH then b = next_byte(state) end -- Exponent consists of one or more decimal digits. if b == nil or not to_dec(b) then return nil, "malformed number" end repeat b = next_byte(state) until b == nil or not to_dec(b) end if not has_digits then return nil, "malformed number" end -- Is it cdata literal? if b == BYTE_i or b == BYTE_I then -- It is complex literal. Skip "i" or "I". next_byte(state) else -- uint64_t and int64_t literals can not be fractional. if not is_float then if b == BYTE_u or b == BYTE_U then -- It may be uint64_t literal. local b1, b2 = sbyte(state.src, state.offset+1, state.offset+2) if (b1 == BYTE_l or b1 == BYTE_L) and (b2 == BYTE_l or b2 == BYTE_L) then -- It is uint64_t literal. next_byte(state, 3) end elseif b == BYTE_l or b == BYTE_L then -- It may be uint64_t or int64_t literal. local b1, b2 = sbyte(state.src, state.offset+1, state.offset+2) if b1 == BYTE_l or b1 == BYTE_L then if b2 == BYTE_u or b2 == BYTE_U then -- It is uint64_t literal. next_byte(state, 3) else -- It is int64_t literal. next_byte(state, 2) end end end end end return "number", ssub(state.src, start, state.offset-1) end local function lex_ident(state) local start = state.offset local b = next_byte(state) while (b ~= nil) and (is_alpha(b) or to_dec(b)) do b = next_byte(state) end local ident = ssub(state.src, start, state.offset-1) if keywords[ident] then return ident else return "name", ident end end local function lex_dash(state) local b = next_byte(state) -- Is it "-" or comment? if b ~= BYTE_DASH then return "-" else -- It is a comment. b = next_byte(state) local start = state.offset -- Is it a long comment? if b == BYTE_OBRACK then local long_bracket b, long_bracket = skip_long_bracket(state) if b == BYTE_OBRACK then return lex_long_string(state, long_bracket, "comment") end end -- Short comment. b = skip_till_newline(state, b) local comment_value = ssub(state.src, start, state.offset-1) skip_newline(state, b) return "comment", comment_value end end local function lex_bracket(state) -- Is it "[" or long string? local b, long_bracket = skip_long_bracket(state) if b == BYTE_OBRACK then return lex_long_string(state, long_bracket, "string") elseif long_bracket == 0 then return "[" else return nil, "invalid long string delimiter" end end local function lex_eq(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "==" else return "=" end end local function lex_lt(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "<=" elseif b == BYTE_LT then next_byte(state) return "<<" else return "<" end end local function lex_gt(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return ">=" elseif b == BYTE_GT then next_byte(state) return ">>" else return ">" end end local function lex_div(state) local b = next_byte(state) if b == BYTE_SLASH then next_byte(state) return "//" else return "/" end end local function lex_ne(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "~=" else return "~" end end local function lex_colon(state) local b = next_byte(state) if b == BYTE_COLON then next_byte(state) return "::" else return ":" end end local function lex_dot(state) local b = next_byte(state) if b == BYTE_DOT then b = next_byte(state) if b == BYTE_DOT then next_byte(state) return "...", "..." else return ".." end elseif b and to_dec(b) then -- Backtrack to dot. return lex_number(state, next_byte(state, -1)) else return "." end end local function lex_any(state, b) next_byte(state) return schar(b) end -- Maps first bytes of tokens to functions that handle them. -- Each handler takes the first byte as an argument. -- Each handler stops at the character after the token and returns the token and, -- optionally, a value associated with the token. -- On error handler returns nil, error message and, optionally, start of reported location as negative offset. local byte_handlers = { [BYTE_DOT] = lex_dot, [BYTE_COLON] = lex_colon, [BYTE_OBRACK] = lex_bracket, [BYTE_QUOTE] = lex_short_string, [BYTE_DQUOTE] = lex_short_string, [BYTE_DASH] = lex_dash, [BYTE_SLASH] = lex_div, [BYTE_EQ] = lex_eq, [BYTE_NE] = lex_ne, [BYTE_LT] = lex_lt, [BYTE_GT] = lex_gt, [BYTE_LDASH] = lex_ident } for b=BYTE_0, BYTE_9 do byte_handlers[b] = lex_number end for b=BYTE_a, BYTE_z do byte_handlers[b] = lex_ident end for b=BYTE_A, BYTE_Z do byte_handlers[b] = lex_ident end local function decimal_escaper(char) return "\\" .. tostring(sbyte(char)) end -- Returns quoted printable representation of s. function lexer.quote(s) return "'" .. s:gsub("[^\32-\126]", decimal_escaper) .. "'" end -- Creates and returns lexer state for source. function lexer.new_state(src) local state = { src = src, line = 1, line_offset = 1, offset = 1 } if ssub(src, 1, 2) == "#!" then -- Skip shebang. skip_newline(state, skip_till_newline(state, next_byte(state, 2))) end return state end function lexer.syntax_error(location, end_column, msg) error({ line = location.line, column = location.column, end_column = end_column, msg = msg}) end -- Looks for next token starting from state.line, state.line_offset, state.offset. -- Returns next token, its value and its location (line, column, offset). -- Sets state.line, state.line_offset, state.offset to token end location + 1. -- On error returns nil, error message, error location (line, column, offset), error end column. function lexer.next_token(state) local b = skip_space(state, sbyte(state.src, state.offset)) -- Save location of token start. local token_line = state.line local token_column = state.offset - state.line_offset + 1 local token_offset = state.offset local token, token_value, err_offset, err_end_column if b == nil then token = "eof" else token, token_value, err_offset = (byte_handlers[b] or lex_any)(state, b) end if err_offset then local token_body = ssub(state.src, state.offset + err_offset, state.offset) token_value = token_value .. " " .. lexer.quote(token_body) token_line = state.line token_column = state.offset - state.line_offset + 1 + err_offset token_offset = state.offset + err_offset err_end_column = token_column + #token_body - 1 end return token, token_value, token_line, token_column, token_offset, err_end_column or token_column end return lexer luacheck-0.13.0/src/luacheck/parser.lua0000644000175000017500000005232512642521554016766 0ustar vsevavsevalocal lexer = require "luacheck.lexer" local utils = require "luacheck.utils" local function new_state(src) return { lexer = lexer.new_state(src), code_lines = {}, -- Set of line numbers containing code. comments = {}, -- Array of {comment = string, location = location}. hanging_semicolons = {} -- Array of locations of semicolons not following an expression or goto. } end local function location(state) return { line = state.line, column = state.column, offset = state.offset } end local function token_body_or_line(state) return state.lexer.src:sub(state.offset, state.lexer.offset - 1):match("^[^\r\n]*") end local function skip_token(state) while true do local err_end_column state.token, state.token_value, state.line, state.column, state.offset, err_end_column = lexer.next_token(state.lexer) if not state.token then lexer.syntax_error(state, err_end_column, state.token_value) elseif state.token == "comment" then state.comments[#state.comments+1] = { contents = state.token_value, location = location(state), end_column = state.column + #token_body_or_line(state) - 1 } else state.code_lines[state.line] = true break end end end local function init_ast_node(node, loc, tag) node.location = loc node.tag = tag return node end local function new_ast_node(state, tag) return init_ast_node({}, location(state), tag) end local token_names = { eof = "", name = "identifier", ["do"] = "'do'", ["end"] = "'end'", ["then"] = "'then'", ["in"] = "'in'", ["until"] = "'until'", ["::"] = "'::'" } local function token_name(token) return token_names[token] or lexer.quote(token) end local function parse_error(state, msg) local token_repr, end_column if state.token == "eof" then token_repr = "" end_column = state.column else token_repr = token_body_or_line(state) end_column = state.column + #token_repr - 1 token_repr = lexer.quote(token_repr) end lexer.syntax_error(state, end_column, msg .. " near " .. token_repr) end local function check_token(state, token) if state.token ~= token then parse_error(state, "expected " .. token_name(token)) end end local function check_and_skip_token(state, token) check_token(state, token) skip_token(state) end local function test_and_skip_token(state, token) if state.token == token then skip_token(state) return true end end local function check_closing_token(state, opening_token, closing_token, opening_line) if state.token ~= closing_token then local err = "expected " .. token_name(closing_token) if opening_line ~= state.line then err = err .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_line) .. ")" end parse_error(state, err) end skip_token(state) end local function check_name(state) check_token(state, "name") return state.token_value end -- If needed, wraps last expression in expressions in "Paren" node. local function opt_add_parens(expressions, is_inside_parentheses) if is_inside_parentheses then local last = expressions[#expressions] if last and last.tag == "Call" or last.tag == "Invoke" or last.tag == "Dots" then expressions[#expressions] = init_ast_node({last}, last.location, "Paren") end end end local parse_block, parse_expression local function parse_expression_list(state) local list = {} local is_inside_parentheses repeat list[#list+1], is_inside_parentheses = parse_expression(state) until not test_and_skip_token(state, ",") opt_add_parens(list, is_inside_parentheses) return list end local function parse_id(state, tag) local ast_node = new_ast_node(state, tag or "Id") ast_node[1] = check_name(state) skip_token(state) -- Skip name. return ast_node end local function atom(tag) return function(state) local ast_node = new_ast_node(state, tag) ast_node[1] = state.token_value skip_token(state) return ast_node end end local simple_expressions = {} simple_expressions.number = atom("Number") simple_expressions.string = atom("String") simple_expressions["nil"] = atom("Nil") simple_expressions["true"] = atom("True") simple_expressions["false"] = atom("False") simple_expressions["..."] = atom("Dots") simple_expressions["{"] = function(state) local ast_node = new_ast_node(state, "Table") local start_line = state.line skip_token(state) local is_inside_parentheses = false repeat if state.token == "}" then break else local lhs, rhs local item_location = location(state) if state.token == "name" then local name = state.token_value skip_token(state) -- Skip name. if test_and_skip_token(state, "=") then -- `name` = `expr`. lhs = init_ast_node({name}, item_location, "String") rhs, is_inside_parentheses = parse_expression(state) else -- `name` is beginning of an expression in array part. -- Backtrack lexer to before name. state.lexer.line = item_location.line state.lexer.line_offset = item_location.offset-item_location.column+1 state.lexer.offset = item_location.offset skip_token(state) -- Load name again. rhs, is_inside_parentheses = parse_expression(state) end else local bracket_line = state.line if test_and_skip_token(state, "[") then -- [ `expr` ] = `expr`. lhs = parse_expression(state) check_closing_token(state, "[", "]", bracket_line) check_and_skip_token(state, "=") rhs = parse_expression(state) else -- Expression in array part. rhs, is_inside_parentheses = parse_expression(state) end end if lhs then -- Pair. ast_node[#ast_node+1] = init_ast_node({lhs, rhs}, item_location, "Pair") else -- Array part item. ast_node[#ast_node+1] = rhs end end until not (test_and_skip_token(state, ",") or test_and_skip_token(state, ";")) check_closing_token(state, "{", "}", start_line) opt_add_parens(ast_node, is_inside_parentheses) return ast_node end -- Parses argument list and the statements. local function parse_function(state, func_location) local paren_line = state.line check_and_skip_token(state, "(") local args = {} if state.token ~= ")" then -- Are there arguments? repeat if state.token == "name" then args[#args+1] = parse_id(state) elseif state.token == "..." then args[#args+1] = simple_expressions["..."](state) break else parse_error(state, "expected argument") end until not test_and_skip_token(state, ",") end check_closing_token(state, "(", ")", paren_line) local body = parse_block(state) local end_location = location(state) check_closing_token(state, "function", "end", func_location.line) return init_ast_node({args, body, end_location = end_location}, func_location, "Function") end simple_expressions["function"] = function(state) local function_location = location(state) skip_token(state) -- Skip "function". return parse_function(state, function_location) end local function parse_prefix_expression(state, kind) if state.token == "name" then return parse_id(state) elseif state.token == "(" then skip_token(state) -- Skip "(" local expression = parse_expression(state) check_and_skip_token(state, ")") return expression else parse_error(state, "expected " .. (kind or "expression")) end end local calls = {} calls["("] = function(state) local paren_line = state.line skip_token(state) -- Skip "(". local args = (state.token == ")") and {} or parse_expression_list(state) check_closing_token(state, "(", ")", paren_line) return args end calls["{"] = function(state) return {simple_expressions[state.token](state)} end calls.string = calls["{"] local suffixes = {} suffixes["."] = function(state, lhs) skip_token(state) -- Skip ".". local rhs = parse_id(state, "String") return init_ast_node({lhs, rhs}, lhs.location, "Index") end suffixes["["] = function(state, lhs) local bracket_line = state.line skip_token(state) -- Skip "[". local rhs = parse_expression(state) check_closing_token(state, "[", "]", bracket_line) return init_ast_node({lhs, rhs}, lhs.location, "Index") end suffixes[":"] = function(state, lhs) skip_token(state) -- Skip ":". local method_name = parse_id(state, "String") local args = (calls[state.token] or parse_error)(state, "expected method arguments") table.insert(args, 1, lhs) table.insert(args, 2, method_name) return init_ast_node(args, lhs.location, "Invoke") end suffixes["("] = function(state, lhs) local args = calls[state.token](state) table.insert(args, 1, lhs) return init_ast_node(args, lhs.location, "Call") end suffixes["{"] = suffixes["("] suffixes.string = suffixes["("] -- Additionally returns whether primary expression is prefix expression. local function parse_primary_expression(state, kind) local expression = parse_prefix_expression(state, kind) local is_prefix = true while true do local handler = suffixes[state.token] if handler then is_prefix = false expression = handler(state, expression) else return expression, is_prefix end end end -- Additionally returns whether simple expression is prefix expression. local function parse_simple_expression(state, kind) return (simple_expressions[state.token] or parse_primary_expression)(state, kind) end local unary_operators = { ["not"] = "not", ["-"] = "unm", -- Not mentioned in Metalua documentation. ["~"] = "bnot", ["#"] = "len" } local unary_priority = 12 local binary_operators = { ["+"] = "add", ["-"] = "sub", ["*"] = "mul", ["%"] = "mod", ["^"] = "pow", ["/"] = "div", ["//"] = "idiv", ["&"] = "band", ["|"] = "bor", ["~"] = "bxor", ["<<"] = "shl", [">>"] = "shr", [".."] = "concat", ["~="] = "ne", ["=="] = "eq", ["<"] = "lt", ["<="] = "le", [">"] = "gt", [">="] = "ge", ["and"] = "and", ["or"] = "or" } local left_priorities = { add = 10, sub = 10, mul = 11, mod = 11, pow = 14, div = 11, idiv = 11, band = 6, bor = 4, bxor = 5, shl = 7, shr = 7, concat = 9, ne = 3, eq = 3, lt = 3, le = 3, gt = 3, ge = 3, ["and"] = 2, ["or"] = 1 } local right_priorities = { add = 10, sub = 10, mul = 11, mod = 11, pow = 13, div = 11, idiv = 11, band = 6, bor = 4, bxor = 5, shl = 7, shr = 7, concat = 8, ne = 3, eq = 3, lt = 3, le = 3, gt = 3, ge = 3, ["and"] = 2, ["or"] = 1 } -- Additionally returns whether subexpression is prefix expression. local function parse_subexpression(state, limit, kind) local expression local is_prefix local unary_operator = unary_operators[state.token] if unary_operator then local unary_location = location(state) skip_token(state) -- Skip operator. local unary_operand = parse_subexpression(state, unary_priority) expression = init_ast_node({unary_operator, unary_operand}, unary_location, "Op") else expression, is_prefix = parse_simple_expression(state, kind) end -- Expand while operators have priorities higher than `limit`. while true do local binary_operator = binary_operators[state.token] if not binary_operator or left_priorities[binary_operator] <= limit then break end is_prefix = false skip_token(state) -- Skip operator. -- Read subexpression with higher priority. local subexpression = parse_subexpression(state, right_priorities[binary_operator]) expression = init_ast_node({binary_operator, expression, subexpression}, expression.location, "Op") end return expression, is_prefix end -- Additionally returns whether expression is inside parentheses. function parse_expression(state, kind, save_first_token) local first_token = token_body_or_line(state) local expression, is_prefix = parse_subexpression(state, 0, kind) expression.first_token = save_first_token and first_token return expression, is_prefix and first_token == "(" end local statements = {} statements["if"] = function(state, loc) local start_line, start_token local next_line, next_token = loc.line, "if" local ast_node = init_ast_node({}, loc, "If") repeat ast_node[#ast_node+1] = parse_expression(state, "condition", true) local branch_location = location(state) check_and_skip_token(state, "then") ast_node[#ast_node+1] = parse_block(state, branch_location) start_line, start_token = next_line, next_token next_line, next_token = state.line, state.token until not test_and_skip_token(state, "elseif") if state.token == "else" then start_line, start_token = next_line, next_token local branch_location = location(state) skip_token(state) ast_node[#ast_node+1] = parse_block(state, branch_location) end check_closing_token(state, start_token, "end", start_line) return ast_node end statements["while"] = function(state, loc) local condition = parse_expression(state, "condition") check_and_skip_token(state, "do") local block = parse_block(state) check_closing_token(state, "while", "end", loc.line) return init_ast_node({condition, block}, loc, "While") end statements["do"] = function(state, loc) local ast_node = init_ast_node(parse_block(state), loc, "Do") check_closing_token(state, "do", "end", loc.line) return ast_node end statements["for"] = function(state, loc) local ast_node = init_ast_node({}, loc) -- Will set ast_node.tag later. local first_var = parse_id(state) if state.token == "=" then -- Numeric "for" loop. ast_node.tag = "Fornum" skip_token(state) ast_node[1] = first_var ast_node[2] = parse_expression(state) check_and_skip_token(state, ",") ast_node[3] = parse_expression(state) if test_and_skip_token(state, ",") then ast_node[4] = parse_expression(state) end check_and_skip_token(state, "do") ast_node[#ast_node+1] = parse_block(state) elseif state.token == "," or state.token == "in" then -- Generic "for" loop. ast_node.tag = "Forin" local iter_vars = {first_var} while test_and_skip_token(state, ",") do iter_vars[#iter_vars+1] = parse_id(state) end ast_node[1] = iter_vars check_and_skip_token(state, "in") ast_node[2] = parse_expression_list(state) check_and_skip_token(state, "do") ast_node[3] = parse_block(state) else parse_error(state, "expected '=', ',' or 'in'") end check_closing_token(state, "for", "end", loc.line) return ast_node end statements["repeat"] = function(state, loc) local block = parse_block(state) check_closing_token(state, "repeat", "until", loc.line) local condition = parse_expression(state, "condition", true) return init_ast_node({block, condition}, loc, "Repeat") end statements["function"] = function(state, loc) local lhs_location = location(state) local lhs = parse_id(state) local self_location while (not self_location) and (state.token == "." or state.token == ":") do self_location = state.token == ":" and location(state) skip_token(state) -- Skip "." or ":". lhs = init_ast_node({lhs, parse_id(state, "String")}, lhs_location, "Index") end local function_node = parse_function(state, loc) if self_location then -- Insert implicit "self" argument. local self_arg = init_ast_node({"self", implicit = true}, self_location, "Id") table.insert(function_node[1], 1, self_arg) end return init_ast_node({{lhs}, {function_node}}, loc, "Set") end statements["local"] = function(state, loc) if state.token == "function" then -- Localrec local function_location = location(state) skip_token(state) -- Skip "function". local var = parse_id(state) local function_node = parse_function(state, function_location) -- Metalua would return {{var}, {function}} for some reason. return init_ast_node({var, function_node}, loc, "Localrec") end local lhs = {} local rhs repeat lhs[#lhs+1] = parse_id(state) until not test_and_skip_token(state, ",") local equals_location = location(state) if test_and_skip_token(state, "=") then rhs = parse_expression_list(state) end -- According to Metalua spec, {lhs} should be returned if there is no rhs. -- Metalua does not follow the spec itself and returns {lhs, {}}. return init_ast_node({lhs, rhs, equals_location = rhs and equals_location}, loc, "Local") end statements["::"] = function(state, loc) local end_column = loc.column + 1 local name = check_name(state) if state.line == loc.line then -- Label name on the same line as opening `::`, pull token end to name end. end_column = state.column + #state.token_value - 1 end skip_token(state) -- Skip label name. if state.line == loc.line then -- Whole label is on one line, pull token end to closing `::` end. end_column = state.column + 1 end check_and_skip_token(state, "::") return init_ast_node({name, end_column = end_column}, loc, "Label") end local closing_tokens = utils.array_to_set({ "end", "eof", "else", "elseif", "until"}) statements["return"] = function(state, loc) if closing_tokens[state.token] or state.token == ";" then -- No return values. return init_ast_node({}, loc, "Return") else return init_ast_node(parse_expression_list(state), loc, "Return") end end statements["break"] = function(_, loc) return init_ast_node({}, loc, "Break") end statements["goto"] = function(state, loc) local name = check_name(state) skip_token(state) -- Skip label name. return init_ast_node({name}, loc, "Goto") end local function parse_expression_statement(state, loc) local lhs repeat local first_token = state.token local first_loc = lhs and location(state) or loc local expected = lhs and "identifier or field" or "statement" local primary_expression, is_prefix = parse_primary_expression(state, expected) if is_prefix and first_token == "(" then -- (expr) is invalid. lexer.syntax_error(first_loc, first_loc.column, "expected " .. expected .. " near '('") end if primary_expression.tag == "Call" or primary_expression.tag == "Invoke" then if lhs then -- This is an assingment, and a call is not a valid lvalue. parse_error(state, "expected call or indexing") else -- It is a call. primary_expression.location = loc return primary_expression end end -- This is an assignment. lhs = lhs or {} lhs[#lhs+1] = primary_expression until not test_and_skip_token(state, ",") local equals_location = location(state) check_and_skip_token(state, "=") local rhs = parse_expression_list(state) return init_ast_node({lhs, rhs, equals_location = equals_location}, loc, "Set") end local function parse_statement(state) local loc = location(state) local statement_parser = statements[state.token] if statement_parser then skip_token(state) return statement_parser(state, loc) else return parse_expression_statement(state, loc) end end function parse_block(state, loc) local block = {location = loc} local after_statement = false while not closing_tokens[state.token] do local first_token = state.token if first_token == ";" then if not after_statement then table.insert(state.hanging_semicolons, location(state)) end skip_token(state) -- Do not allow several semicolons in a row, even if the first one is valid. after_statement = false else first_token = state.token_value or first_token local statement = parse_statement(state) after_statement = true statement.first_token = first_token block[#block+1] = statement if first_token == "return" then -- "return" must be the last statement. -- However, one ";" after it is allowed. test_and_skip_token(state, ";") if not closing_tokens[state.token] then parse_error(state, "expected end of block") end end end end return block end -- Parses source string. -- Returns AST (in almost MetaLua format), array of comments - tables {comment = string, location = location}, -- set of line numbers containing code, and array of locations of empty statements (semicolons). -- On error throws {line = line, column = column, end_column = end_column, msg = msg} local function parse(src) local state = new_state(src) skip_token(state) local ast = parse_block(state) check_token(state, "eof") return ast, state.comments, state.code_lines, state.hanging_semicolons end return parse luacheck-0.13.0/src/luacheck/utils.lua0000644000175000017500000001334312642521554016627 0ustar vsevavsevalocal utils = {} utils.dir_sep = package.config:sub(1,1) utils.is_windows = utils.dir_sep == "\\" local bom = "\239\187\191" -- Returns all contents of file (path or file handler) or nil. function utils.read_file(file) local handler if type(file) == "string" then handler = io.open(file, "rb") if not handler then return nil end else handler = file end local res = handler:read("*a") handler:close() -- Use :len() instead of # operator because in some environments -- string library is patched to handle UTF. if res and res:sub(1, bom:len()) == bom then res = res:sub(bom:len() + 1) end return res end -- luacheck: push -- luacheck: compat if _VERSION:find "5.1" then -- Loads Lua source string in an environment, returns function or nil, error. function utils.load(src, env, chunkname) local func, err = loadstring(src, chunkname) if func then if env then setfenv(func, env) end return func else return nil, err end end else -- Loads Lua source string in an environment, returns function or nil, error. function utils.load(src, env, chunkname) return load(src, chunkname, "t", env or _ENV) end end -- luacheck: pop -- Loads config containing assignments to global variables from path. -- Returns config table and return value of config or nil and error message -- ("I/O" or "syntax" or "runtime"). function utils.load_config(path, env) env = env or {} local src = utils.read_file(path) if not src then return nil, "I/O" end local func = utils.load(src, env) if not func then return nil, "syntax" end local ok, res = pcall(func) if not ok then return nil, "runtime" end return env, res end function utils.array_to_set(array) local set = {} for index, value in ipairs(array) do set[value] = index end return set end function utils.concat_arrays(array) local res = {} for _, subarray in ipairs(array) do for _, item in ipairs(subarray) do table.insert(res, item) end end return res end function utils.update(t1, t2) for k, v in pairs(t2) do t1[k] = v end return t1 end local class_metatable = {} function class_metatable.__call(class, ...) local obj = setmetatable({}, class) if class.__init then class.__init(obj, ...) end return obj end function utils.class() local class = setmetatable({}, class_metatable) class.__index = class return class end utils.Stack = utils.class() function utils.Stack:__init() self.size = 0 end function utils.Stack:push(value) self.size = self.size + 1 self[self.size] = value self.top = value end function utils.Stack:pop() local value = self[self.size] self[self.size] = nil self.size = self.size - 1 self.top = self[self.size] return value end local function error_handler(err) return { err = err, traceback = debug.traceback() } end -- Calls f with arg, returns what it does. -- If f throws a table, returns nil, the table. -- If f throws not a table, rethrows. function utils.pcall(f, arg) local function task() return f(arg) end local ok, res = xpcall(task, error_handler) if ok then return res elseif type(res.err) == "table" then return nil, res.err else error(tostring(res.err) .. "\n" .. res.traceback, 0) end end local function ripairs_iterator(array, i) if i == 1 then return nil else i = i - 1 return i, array[i] end end function utils.ripairs(array) return ripairs_iterator, array, #array + 1 end function utils.after(str, pattern) local _, last_matched_index = str:find(pattern) if last_matched_index then return str:sub(last_matched_index + 1) end end function utils.strip(str) local _, last_start_space = str:find("^%s*") local first_end_space = str:find("%s*$") return str:sub(last_start_space + 1, first_end_space - 1) end -- `sep` must be nil or a single character. Behaves like python's `str.split`. function utils.split(str, sep) local parts = {} local pattern if sep then pattern = sep .. "([^" .. sep .. "]*)" str = sep .. str else pattern = "%S+" end for part in str:gmatch(pattern) do table.insert(parts, part) end return parts end -- Behaves like string.match, except it normally returns boolean and -- throws a table {pattern = pattern} on invalid pattern. -- The error message turns into original error when tostring is used on it, -- to ensure behaviour is predictable when luacheck is used as a module. function utils.pmatch(str, pattern) assert(type(str) == "string") assert(type(pattern) == "string") local ok, res = pcall(string.match, str, pattern) if not ok then error(setmetatable({pattern = pattern}, {__tostring = function() return res end})) else return not not res end end -- Maps func over array. function utils.map(func, array) local res = {} for i, item in ipairs(array) do res[i] = func(item) end return res end -- Returns predicate checking type. function utils.has_type(type_) return function(x) return type(x) == type_ end end -- Returns predicate checking that value is an array with -- elements of type. function utils.array_of(type_) return function(x) if type(x) ~= "table" then return false end for _, item in ipairs(x) do if type(item) ~= type_ then return false end end return true end end -- Returns predicate chacking if value satisfies on of predicates. function utils.either(pred1, pred2) return function(x) return pred1(x) or pred2(x) end end return utils luacheck-0.13.0/src/luacheck/options.lua0000644000175000017500000002034112642521554017156 0ustar vsevavsevalocal options = {} local utils = require "luacheck.utils" local stds = require "luacheck.stds" local boolean = utils.has_type("boolean") local array_of_strings = utils.array_of("string") function options.split_std(std) local parts = utils.split(std, "+") if parts[1]:match("^%s*$") then parts.add = true table.remove(parts, 1) end for i, part in ipairs(parts) do parts[i] = utils.strip(part) if not stds[parts[i]] then return end end return parts end local function std_or_array_of_strings(x) return array_of_strings(x) or (type(x) == "string" and options.split_std(x)) end function options.add_order(option_set) local opts = {} for option in pairs(option_set) do if type(option) == "string" then table.insert(opts, option) end end table.sort(opts) utils.update(option_set, opts) end options.nullary_inline_options = { global = boolean, unused = boolean, redefined = boolean, unused_args = boolean, unused_secondaries = boolean, self = boolean, compat = boolean, allow_defined = boolean, allow_defined_top = boolean, module = boolean } options.variadic_inline_options = { globals = array_of_strings, read_globals = array_of_strings, new_globals = array_of_strings, new_read_globals = array_of_strings, ignore = array_of_strings, enable = array_of_strings, only = array_of_strings } options.all_options = { std = std_or_array_of_strings, inline = boolean } utils.update(options.all_options, options.nullary_inline_options) utils.update(options.all_options, options.variadic_inline_options) options.add_order(options.all_options) -- Returns true if opts is valid option_set. -- Otherwise returns false and, optionally, name of the problematic option. function options.validate(option_set, opts) if opts == nil then return true end local ok, is_valid, invalid_opt = pcall(function() assert(type(opts) == "table") for _, option in ipairs(option_set) do if opts[option] ~= nil then if not option_set[option](opts[option]) then return false, option end end end return true end) return ok and is_valid, invalid_opt end -- Option stack is an array of options with options closer to end -- overriding options closer to beginning. -- Returns sets of std globals and read-only std globals from option stack. -- Std globals can be set using compat option (sets std to stds.max) or std option. -- If std is a table, array part contains read-only globals, hash part - regular globals as keys. -- If it is a string, it must contain names of standard sets separated by +. -- If prefixed with +, standard sets will be added on top of existing ones. local function get_std_sets(opts_stack) local base_std local add_stds = {} local no_compat = false for _, opts in utils.ripairs(opts_stack) do if opts.compat and not no_compat then base_std = "max" break elseif opts.compat == false then no_compat = true end if opts.std then if type(opts.std) == "table" then base_std = opts.std break else local parts = options.split_std(opts.std) for _, part in ipairs(parts) do table.insert(add_stds, part) end if not parts.add then base_std = {} break end end end end table.insert(add_stds, base_std or "_G") local std_globals = {} local std_read_globals = {} for _, add_std in ipairs(add_stds) do add_std = stds[add_std] or add_std for _, read_global in ipairs(add_std) do std_read_globals[read_global] = true end for global in pairs(add_std) do if type(global) == "string" then std_globals[global] = true end end end return std_globals, std_read_globals end local function get_globals(opts_stack, key) local globals_lists = {} for _, opts in utils.ripairs(opts_stack) do if opts["new_" .. key] then table.insert(globals_lists, opts["new_" .. key]) break end if opts[key] then table.insert(globals_lists, opts[key]) end end return utils.concat_arrays(globals_lists) end local function get_boolean_opt(opts_stack, option) for _, opts in utils.ripairs(opts_stack) do if opts[option] ~= nil then return opts[option] end end end local function anchor_pattern(pattern, only_start) if not pattern then return end if pattern:sub(1, 1) == "^" or pattern:sub(-1) == "$" then return pattern else return "^" .. pattern .. (only_start and "" or "$") end end -- Returns {pair of normalized patterns for code and name}. -- `pattern` can be: -- string containing '/': first part matches warning code, second - variable name; -- string containing letters: matches variable name; -- otherwise: matches warning code. -- Unless anchored by user, pattern for name is anchored from both sides -- and pattern for code is only anchored at the beginning. local function normalize_pattern(pattern) local code_pattern, name_pattern local slash_pos = pattern:find("/") if slash_pos then code_pattern = pattern:sub(1, slash_pos - 1) name_pattern = pattern:sub(slash_pos + 1) elseif pattern:find("[_a-zA-Z]") then name_pattern = pattern else code_pattern = pattern end return {anchor_pattern(code_pattern, true), anchor_pattern(name_pattern)} end -- From most specific to less specific, pairs {option, pattern}. -- Applying macros in order is required to get deterministic resuls -- and get sensible results when intersecting macros are used. -- E.g. unused = false, unused_args = true should leave unused args enabled. local macros = { {"unused_args", "21[23]"}, {"global", "1"}, {"unused", "[23]"}, {"redefined", "4"} } -- Returns array of rules which should be applied in order. -- A rule is a table {{pattern*}, type}. -- `pattern` is a non-normalized pattern. -- `type` can be "enable", "disable" or "only". local function get_rules(opts_stack) local rules = {} local used_macros = {} for _, opts in utils.ripairs(opts_stack) do for _, macro_info in ipairs(macros) do local option, pattern = macro_info[1], macro_info[2] if not used_macros[option] then if opts[option] ~= nil then table.insert(rules, {{pattern}, opts[option] and "enable" or "disable"}) used_macros[option] = true end end end if opts.ignore then table.insert(rules, {opts.ignore, "disable"}) end if opts.only then table.insert(rules, {opts.only, "only"}) end if opts.enable then table.insert(rules, {opts.enable, "enable"}) end end return rules end local function normalize_patterns(rules) local res = {} for i, rule in ipairs(rules) do res[i] = {{}, rule[2]} for j, pattern in ipairs(rule[1]) do res[i][1][j] = normalize_pattern(pattern) end end return res end -- Returns normalized options. -- Normalized options have fields: -- globals: set of strings; -- read_globals: subset of globals; -- unused_secondaries, module, allow_defined, allow_defined_top: booleans; -- rules: see get_rules. function options.normalize(opts_stack) local res = {} res.globals = utils.array_to_set(get_globals(opts_stack, "globals")) res.read_globals = utils.array_to_set(get_globals(opts_stack, "read_globals")) local std_globals, std_read_globals = get_std_sets(opts_stack) utils.update(res.globals, std_globals) utils.update(res.read_globals, std_read_globals) for k in pairs(res.globals) do res.read_globals[k] = nil end utils.update(res.globals, res.read_globals) for i, option in ipairs {"unused_secondaries", "self", "inline", "module", "allow_defined", "allow_defined_top"} do local value = get_boolean_opt(opts_stack, option) if value == nil then res[option] = i < 4 else res[option] = value end end res.rules = normalize_patterns(get_rules(opts_stack)) return res end return options luacheck-0.13.0/src/luacheck/fs.lua0000644000175000017500000001030512642521554016072 0ustar vsevavsevalocal fs = {} local utils = require "luacheck.utils" fs.has_lfs, fs.lfs = pcall(require, "lfs") local function ensure_dir_sep(path) if path:sub(-1) ~= utils.dir_sep then return path .. utils.dir_sep end return path end if utils.is_windows then function fs.split_base(path) if path:match("^%a:\\") then return path:sub(1, 3), path:sub(4) else -- Disregard UNC stuff for now. return "", path end end else function fs.split_base(path) if path:match("^/") then if path:match("^//") then return "//", path:sub(3) else return "/", path:sub(2) end else return "", path end end end local function is_absolute(path) return fs.split_base(path) ~= "" end function fs.normalize(path) local base, rest = fs.split_base(path) rest = rest:gsub("[/\\]", utils.dir_sep) local parts = {} for part in rest:gmatch("[^"..utils.dir_sep.."]+") do if part ~= "." then if part == ".." and #parts > 0 and parts[#parts] ~= ".." then parts[#parts] = nil else parts[#parts + 1] = part end end end if base == "" and #parts == 0 then return "." else return base..table.concat(parts, utils.dir_sep) end end function fs.join(base, path) if base == "" or is_absolute(path) then return path else return ensure_dir_sep(base)..path end end function fs.is_subpath(path, subpath) local base1, rest1 = fs.split_base(path) local base2, rest2 = fs.split_base(subpath) if base1 ~= base2 then return false end if rest2:sub(1, #rest1) ~= rest1 then return false end return rest1 == rest2 or rest2:sub(#rest1 + 1, #rest1 + 1) == utils.dir_sep end -- Searches for file starting from path, going up until the file -- is found or root directory is reached. -- Path must be absolute. -- Returns absolute and relative paths to directory containing file or nil. function fs.find_file(path, file) if is_absolute(file) then return fs.is_file(file) and path, "" end path = fs.normalize(path) local base, rest = fs.split_base(path) local rel_path = "" while true do if fs.is_file(fs.join(base..rest, file)) then return base..rest, rel_path elseif rest == "" then break end rest = rest:match("^(.*)"..utils.dir_sep..".*$") or "" rel_path = rel_path..".."..utils.dir_sep end end if not fs.has_lfs then function fs.is_dir(_) return false end function fs.is_file(path) local fh = io.open(path) if fh then fh:close() return true else return false end end function fs.extract_files(_, _) return {} end function fs.mtime(_) return 0 end local pwd_command = utils.is_windows and "cd" or "pwd" function fs.current_dir() local fh = io.popen(pwd_command) local current_dir = fh:read("*a") fh:close() -- Remove extra newline at the end. return ensure_dir_sep(current_dir:sub(1, -2)) end return fs end -- Returns whether path points to a directory. function fs.is_dir(path) return fs.lfs.attributes(path, "mode") == "directory" end -- Returns whether path points to a file. function fs.is_file(path) return fs.lfs.attributes(path, "mode") == "file" end -- Returns list of all files in directory matching pattern. function fs.extract_files(dir_path, pattern) local res = {} local function scan(dir) for path in fs.lfs.dir(dir) do if path ~= "." and path ~= ".." then local full_path = dir .. utils.dir_sep .. path if fs.is_dir(full_path) then scan(full_path) elseif path:match(pattern) and fs.is_file(full_path) then table.insert(res, full_path) end end end end scan(dir_path) table.sort(res) return res end -- Returns modification time for a file. function fs.mtime(path) return fs.lfs.attributes(path, "modification") end -- Returns absolute path to current working directory. function fs.current_dir() return ensure_dir_sep(assert(fs.lfs.currentdir())) end return fs luacheck-0.13.0/src/luacheck/argparse.lua0000644000175000017500000007033112642521554017273 0ustar vsevavseva-- The MIT License (MIT) -- Copyright (c) 2013 - 2015 Peter Melnichenko -- Permission is hereby granted, free of charge, to any person obtaining a copy of -- this software and associated documentation files (the "Software"), to deal in -- the Software without restriction, including without limitation the rights to -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -- the Software, and to permit persons to whom the Software is furnished to do so, -- subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all -- copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. local function deep_update(t1, t2) for k, v in pairs(t2) do if type(v) == "table" then v = deep_update({}, v) end t1[k] = v end return t1 end -- A property is a tuple {name, callback}. -- properties.args is number of properties that can be set as arguments -- when calling an object. local function class(prototype, properties, parent) -- Class is the metatable of its instances. local cl = {} cl.__index = cl if parent then cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) else cl.__prototype = prototype end if properties then local names = {} -- Create setter methods and fill set of property names. for _, property in ipairs(properties) do local name, callback = property[1], property[2] cl[name] = function(self, value) if not callback(self, value) then self["_" .. name] = value end return self end names[name] = true end function cl.__call(self, ...) -- When calling an object, if the first argument is a table, -- interpret keys as property names, else delegate arguments -- to corresponding setters in order. if type((...)) == "table" then for name, value in pairs((...)) do if names[name] then self[name](self, value) end end else local nargs = select("#", ...) for i, property in ipairs(properties) do if i > nargs or i > properties.args then break end local arg = select(i, ...) if arg ~= nil then self[property[1]](self, arg) end end end return self end end -- If indexing class fails, fallback to its parent. local class_metatable = {} class_metatable.__index = parent function class_metatable.__call(self, ...) -- Calling a class returns its instance. -- Arguments are delegated to the instance. local object = deep_update({}, self.__prototype) setmetatable(object, self) return object(...) end return setmetatable(cl, class_metatable) end local function typecheck(name, types, value) for _, type_ in ipairs(types) do if type(value) == type_ then return true end end error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) end local function typechecked(name, ...) local types = {...} return {name, function(_, value) typecheck(name, types, value) end} end local multiname = {"name", function(self, value) typecheck("name", {"string"}, value) for alias in value:gmatch("%S+") do self._name = self._name or alias table.insert(self._aliases, alias) end -- Do not set _name as with other properties. return true end} local function parse_boundaries(str) if tonumber(str) then return tonumber(str), tonumber(str) end if str == "*" then return 0, math.huge end if str == "+" then return 1, math.huge end if str == "?" then return 0, 1 end if str:match "^%d+%-%d+$" then local min, max = str:match "^(%d+)%-(%d+)$" return tonumber(min), tonumber(max) end if str:match "^%d+%+$" then local min = str:match "^(%d+)%+$" return tonumber(min), math.huge end end local function boundaries(name) return {name, function(self, value) typecheck(name, {"number", "string"}, value) local min, max = parse_boundaries(value) if not min then error(("bad property '%s'"):format(name)) end self["_min" .. name], self["_max" .. name] = min, max end} end local actions = {} local option_action = {"action", function(_, value) typecheck("action", {"function", "string"}, value) if type(value) == "string" and not actions[value] then error(("unknown action '%s'"):format(value)) end end} local option_init = {"init", function(self) self._has_init = true end} local option_default = {"default", function(self, value) if type(value) ~= "string" then self._init = value self._has_init = true return true end end} local add_help = {"add_help", function(self, value) typecheck("add_help", {"boolean", "string", "table"}, value) if self._has_help then table.remove(self._options) self._has_help = false end if value then local help = self:flag() :description "Show this help message and exit." :action(function() print(self:get_help()) os.exit(0) end) if value ~= true then help = help(value) end if not help._name then help "-h" "--help" end self._has_help = true end end} local Parser = class({ _arguments = {}, _options = {}, _commands = {}, _mutexes = {}, _require_command = true, _handle_options = true }, { args = 3, typechecked("name", "string"), typechecked("description", "string"), typechecked("epilog", "string"), typechecked("usage", "string"), typechecked("help", "string"), typechecked("require_command", "boolean"), typechecked("handle_options", "boolean"), typechecked("action", "function"), add_help }) local Command = class({ _aliases = {} }, { args = 3, multiname, typechecked("description", "string"), typechecked("epilog", "string"), typechecked("target", "string"), typechecked("usage", "string"), typechecked("help", "string"), typechecked("require_command", "boolean"), typechecked("handle_options", "boolean"), typechecked("action", "function"), add_help }, Parser) local Argument = class({ _minargs = 1, _maxargs = 1, _mincount = 1, _maxcount = 1, _defmode = "unused", _show_default = true }, { args = 5, typechecked("name", "string"), typechecked("description", "string"), option_default, typechecked("convert", "function", "table"), boundaries("args"), typechecked("target", "string"), typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("argname", "string", "table"), option_action, option_init }) local Option = class({ _aliases = {}, _mincount = 0, _overwrite = true }, { args = 6, multiname, typechecked("description", "string"), option_default, typechecked("convert", "function", "table"), boundaries("args"), boundaries("count"), typechecked("target", "string"), typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("overwrite", "boolean"), typechecked("argname", "string", "table"), option_action, option_init }, Argument) function Argument:_get_argument_list() local buf = {} local i = 1 while i <= math.min(self._minargs, 3) do local argname = self:_get_argname(i) if self._default and self._defmode:find "a" then argname = "[" .. argname .. "]" end table.insert(buf, argname) i = i+1 end while i <= math.min(self._maxargs, 3) do table.insert(buf, "[" .. self:_get_argname(i) .. "]") i = i+1 if self._maxargs == math.huge then break end end if i < self._maxargs then table.insert(buf, "...") end return buf end function Argument:_get_usage() local usage = table.concat(self:_get_argument_list(), " ") if self._default and self._defmode:find "u" then if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then usage = "[" .. usage .. "]" end end return usage end function actions.store_true(result, target) result[target] = true end function actions.store_false(result, target) result[target] = false end function actions.store(result, target, argument) result[target] = argument end function actions.count(result, target, _, overwrite) if not overwrite then result[target] = result[target] + 1 end end function actions.append(result, target, argument, overwrite) result[target] = result[target] or {} table.insert(result[target], argument) if overwrite then table.remove(result[target], 1) end end function actions.concat(result, target, arguments, overwrite) if overwrite then error("'concat' action can't handle too many invocations") end result[target] = result[target] or {} for _, argument in ipairs(arguments) do table.insert(result[target], argument) end end function Argument:_get_action() local action, init if self._maxcount == 1 then if self._maxargs == 0 then action, init = "store_true", nil else action, init = "store", nil end else if self._maxargs == 0 then action, init = "count", 0 else action, init = "append", {} end end if self._action then action = self._action end if self._has_init then init = self._init end if type(action) == "string" then action = actions[action] end return action, init end -- Returns placeholder for `narg`-th argument. function Argument:_get_argname(narg) local argname = self._argname or self:_get_default_argname() if type(argname) == "table" then return argname[narg] else return argname end end function Argument:_get_default_argname() return "<" .. self._name .. ">" end function Option:_get_default_argname() return "<" .. self:_get_default_target() .. ">" end -- Returns label to be shown in the help message. function Argument:_get_label() return self._name end function Option:_get_label() local variants = {} local argument_list = self:_get_argument_list() table.insert(argument_list, 1, nil) for _, alias in ipairs(self._aliases) do argument_list[1] = alias table.insert(variants, table.concat(argument_list, " ")) end return table.concat(variants, ", ") end function Command:_get_label() return table.concat(self._aliases, ", ") end function Argument:_get_description() if self._default and self._show_default then if self._description then return ("%s (default: %s)"):format(self._description, self._default) else return ("default: %s"):format(self._default) end else return self._description or "" end end function Command:_get_description() return self._description or "" end function Option:_get_usage() local usage = self:_get_argument_list() table.insert(usage, 1, self._name) usage = table.concat(usage, " ") if self._mincount == 0 or self._default then usage = "[" .. usage .. "]" end return usage end function Argument:_get_default_target() return self._name end function Option:_get_default_target() local res for _, alias in ipairs(self._aliases) do if alias:sub(1, 1) == alias:sub(2, 2) then res = alias:sub(3) break end end res = res or self._name:sub(2) return (res:gsub("-", "_")) end function Option:_is_vararg() return self._maxargs ~= self._minargs end function Parser:_get_fullname() local parent = self._parent local buf = {self._name} while parent do table.insert(buf, 1, parent._name) parent = parent._parent end return table.concat(buf, " ") end function Parser:_update_charset(charset) charset = charset or {} for _, command in ipairs(self._commands) do command:_update_charset(charset) end for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do charset[alias:sub(1, 1)] = true end end return charset end function Parser:argument(...) local argument = Argument(...) table.insert(self._arguments, argument) return argument end function Parser:option(...) local option = Option(...) if self._has_help then table.insert(self._options, #self._options, option) else table.insert(self._options, option) end return option end function Parser:flag(...) return self:option():args(0)(...) end function Parser:command(...) local command = Command():add_help(true)(...) command._parent = self table.insert(self._commands, command) return command end function Parser:mutex(...) local options = {...} for i, option in ipairs(options) do assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i)) end table.insert(self._mutexes, options) return self end local max_usage_width = 70 local usage_welcome = "Usage: " function Parser:get_usage() if self._usage then return self._usage end local lines = {usage_welcome .. self:_get_fullname()} local function add(s) if #lines[#lines]+1+#s <= max_usage_width then lines[#lines] = lines[#lines] .. " " .. s else lines[#lines+1] = (" "):rep(#usage_welcome) .. s end end -- This can definitely be refactored into something cleaner local mutex_options = {} local vararg_mutexes = {} -- First, put mutexes which do not contain vararg options and remember those which do for _, mutex in ipairs(self._mutexes) do local buf = {} local is_vararg = false for _, option in ipairs(mutex) do if option:_is_vararg() then is_vararg = true end table.insert(buf, option:_get_usage()) mutex_options[option] = true end local repr = "(" .. table.concat(buf, " | ") .. ")" if is_vararg then table.insert(vararg_mutexes, repr) else add(repr) end end -- Second, put regular options for _, option in ipairs(self._options) do if not mutex_options[option] and not option:_is_vararg() then add(option:_get_usage()) end end -- Put positional arguments for _, argument in ipairs(self._arguments) do add(argument:_get_usage()) end -- Put mutexes containing vararg options for _, mutex_repr in ipairs(vararg_mutexes) do add(mutex_repr) end for _, option in ipairs(self._options) do if not mutex_options[option] and option:_is_vararg() then add(option:_get_usage()) end end if #self._commands > 0 then if self._require_command then add("") else add("[]") end add("...") end return table.concat(lines, "\n") end local margin_len = 3 local margin_len2 = 25 local margin = (" "):rep(margin_len) local margin2 = (" "):rep(margin_len2) local function make_two_columns(s1, s2) if s2 == "" then return margin .. s1 end s2 = s2:gsub("\n", "\n" .. margin2) if #s1 < (margin_len2-margin_len) then return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2 else return margin .. s1 .. "\n" .. margin2 .. s2 end end function Parser:get_help() if self._help then return self._help end local blocks = {self:get_usage()} if self._description then table.insert(blocks, self._description) end local labels = {"Arguments:", "Options:", "Commands:"} for i, elements in ipairs{self._arguments, self._options, self._commands} do if #elements > 0 then local buf = {labels[i]} for _, element in ipairs(elements) do table.insert(buf, make_two_columns(element:_get_label(), element:_get_description())) end table.insert(blocks, table.concat(buf, "\n")) end end if self._epilog then table.insert(blocks, self._epilog) end return table.concat(blocks, "\n\n") end local function get_tip(context, wrong_name) local context_pool = {} local possible_name local possible_names = {} for name in pairs(context) do if type(name) == "string" then for i = 1, #name do possible_name = name:sub(1, i - 1) .. name:sub(i + 1) if not context_pool[possible_name] then context_pool[possible_name] = {} end table.insert(context_pool[possible_name], name) end end end for i = 1, #wrong_name + 1 do possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) if context[possible_name] then possible_names[possible_name] = true elseif context_pool[possible_name] then for _, name in ipairs(context_pool[possible_name]) do possible_names[name] = true end end end local first = next(possible_names) if first then if next(possible_names, first) then local possible_names_arr = {} for name in pairs(possible_names) do table.insert(possible_names_arr, "'" .. name .. "'") end table.sort(possible_names_arr) return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" else return "\nDid you mean '" .. first .. "'?" end else return "" end end local ElementState = class({ invocations = 0 }) function ElementState:__call(state, element) self.state = state self.result = state.result self.element = element self.target = element._target or element:_get_default_target() self.action, self.result[self.target] = element:_get_action() return self end function ElementState:error(fmt, ...) self.state:error(fmt, ...) end function ElementState:convert(argument) local converter = self.element._convert if converter then local ok, err if type(converter) == "function" then ok, err = converter(argument) else ok = converter[argument] end if ok == nil then self:error(err and "%s" or "malformed argument '%s'", err or argument) end argument = ok end return argument end function ElementState:default(mode) return self.element._defmode:find(mode) and self.element._default end local function bound(noun, min, max, is_max) local res = "" if min ~= max then res = "at " .. (is_max and "most" or "least") .. " " end local number = is_max and max or min return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") end function ElementState:invoke(alias) self.open = true self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) self.overwrite = false if self.invocations >= self.element._maxcount then if self.element._overwrite then self.overwrite = true else self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true)) end else self.invocations = self.invocations + 1 end self.args = {} if self.element._maxargs <= 0 then self:close() end return self.open end function ElementState:pass(argument) argument = self:convert(argument) table.insert(self.args, argument) if #self.args >= self.element._maxargs then self:close() end return self.open end function ElementState:complete_invocation() while #self.args < self.element._minargs do self:pass(self.element._default) end end function ElementState:close() if self.open then self.open = false if #self.args < self.element._minargs then if self:default("a") then self:complete_invocation() else if #self.args == 0 then if getmetatable(self.element) == Argument then self:error("missing %s", self.name) elseif self.element._maxargs == 1 then self:error("%s requires an argument", self.name) end end self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) end end local args = self.args if self.element._maxargs <= 1 then args = args[1] end if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then args = self.args end self.action(self.result, self.target, args, self.overwrite) end end local ParseState = class({ result = {}, options = {}, arguments = {}, argument_i = 1, element_to_mutexes = {}, mutex_to_used_option = {}, command_actions = {} }) function ParseState:__call(parser, error_handler) self.parser = parser self.error_handler = error_handler self.charset = parser:_update_charset() self:switch(parser) return self end function ParseState:error(fmt, ...) self.error_handler(self.parser, fmt:format(...)) end function ParseState:switch(parser) self.parser = parser if parser._action then table.insert(self.command_actions, parser._action) end for _, option in ipairs(parser._options) do option = ElementState(self, option) table.insert(self.options, option) for _, alias in ipairs(option.element._aliases) do self.options[alias] = option end end for _, mutex in ipairs(parser._mutexes) do for _, option in ipairs(mutex) do if not self.element_to_mutexes[option] then self.element_to_mutexes[option] = {} end table.insert(self.element_to_mutexes[option], mutex) end end for _, argument in ipairs(parser._arguments) do argument = ElementState(self, argument) table.insert(self.arguments, argument) argument:invoke() end self.handle_options = parser._handle_options self.argument = self.arguments[self.argument_i] self.commands = parser._commands for _, command in ipairs(self.commands) do for _, alias in ipairs(command._aliases) do self.commands[alias] = command end end end function ParseState:get_option(name) local option = self.options[name] if not option then self:error("unknown option '%s'%s", name, get_tip(self.options, name)) else return option end end function ParseState:get_command(name) local command = self.commands[name] if not command then if #self.commands > 0 then self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) else self:error("too many arguments") end else return command end end function ParseState:invoke(option, name) self:close() if self.element_to_mutexes[option.element] then for _, mutex in ipairs(self.element_to_mutexes[option.element]) do local used_option = self.mutex_to_used_option[mutex] if used_option and used_option ~= option then self:error("option '%s' can not be used together with %s", name, used_option.name) else self.mutex_to_used_option[mutex] = option end end end if option:invoke(name) then self.option = option end end function ParseState:pass(arg) if self.option then if not self.option:pass(arg) then self.option = nil end elseif self.argument then if not self.argument:pass(arg) then self.argument_i = self.argument_i + 1 self.argument = self.arguments[self.argument_i] end else local command = self:get_command(arg) self.result[command._target or command._name] = true self:switch(command) end end function ParseState:close() if self.option then self.option:close() self.option = nil end end function ParseState:finalize() self:close() for i = self.argument_i, #self.arguments do local argument = self.arguments[i] if #argument.args == 0 and argument:default("u") then argument:complete_invocation() else argument:close() end end if self.parser._require_command and #self.commands > 0 then self:error("a command is required") end for _, option in ipairs(self.options) do local name = option.name or ("option '%s'"):format(option.element._name) if option.invocations == 0 then if option:default("u") then option:invoke(name) option:complete_invocation() option:close() end end local mincount = option.element._mincount if option.invocations < mincount then if option:default("a") then while option.invocations < mincount do option:invoke(name) option:close() end elseif option.invocations == 0 then self:error("missing %s", name) else self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount)) end end end for i = #self.command_actions, 1, -1 do self.command_actions[i](self.result) end end function ParseState:parse(args) for _, arg in ipairs(args) do local plain = true if self.handle_options then local first = arg:sub(1, 1) if self.charset[first] then if #arg > 1 then plain = false if arg:sub(2, 2) == first then if #arg == 2 then self:close() self.handle_options = false else local equals = arg:find "=" if equals then local name = arg:sub(1, equals - 1) local option = self:get_option(name) if option.element._maxargs <= 0 then self:error("option '%s' does not take arguments", name) end self:invoke(option, name) self:pass(arg:sub(equals + 1)) else local option = self:get_option(arg) self:invoke(option, arg) end end else for i = 2, #arg do local name = first .. arg:sub(i, i) local option = self:get_option(name) self:invoke(option, name) if i ~= #arg and option.element._maxargs > 0 then self:pass(arg:sub(i + 1)) break end end end end end end if plain then self:pass(arg) end end self:finalize() return self.result end function Parser:error(msg) io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) os.exit(1) end -- Compatibility with strict.lua and other checkers: local default_cmdline = rawget(_G, "arg") or {} function Parser:_parse(args, error_handler) return ParseState(self, error_handler):parse(args or default_cmdline) end function Parser:parse(args) return self:_parse(args, self.error) end local function xpcall_error_handler(err) return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) end function Parser:pparse(args) local parse_error local ok, result = xpcall(function() return self:_parse(args, function(_, err) parse_error = err error(err, 0) end) end, xpcall_error_handler) if ok then return true, result elseif not parse_error then error(result, 0) else return false, parse_error end end return function(...) return Parser(default_cmdline[0]):add_help(true)(...) end luacheck-0.13.0/src/luacheck/init.lua0000644000175000017500000001015712642521554016432 0ustar vsevavsevalocal check = require "luacheck.check" local filter = require "luacheck.filter" local options = require "luacheck.options" local format = require "luacheck.format" local utils = require "luacheck.utils" local luacheck = { _VERSION = "0.13.0" } local function raw_validate_options(fname, opts) assert(opts == nil or type(opts) == "table", ("bad argument #2 to '%s' (table or nil expected, got %s)"):format(fname, type(opts)) ) local ok, invalid_field = options.validate(options.all_options, opts) if not ok then if invalid_field then error(("bad argument #2 to '%s' (invalid value of option '%s')"):format(fname, invalid_field)) else error(("bad argument #2 to '%s'"):format(fname)) end end end local function validate_options(fname, items, opts) raw_validate_options(fname, opts) if opts ~= nil then for i in ipairs(items) do raw_validate_options(fname, opts[i]) if opts[i] ~= nil then for _, nested_opts in ipairs(opts[i]) do raw_validate_options(fname, nested_opts) end end end end end -- Returns report for a string. Report is an array of warnings and errors. function luacheck.get_report(src) assert(type(src) == "string", ("bad argument #1 to 'luacheck.get_report' (string expected, got %s)"):format(type(src))) return check(src) end -- Applies options to reports. Reports with .fatal field are unchanged. -- Options are applied to reports[i] in order: options, options[i], options[i][1], options[i][2], ... -- Returns new array of reports, adds .warnings, .errors and .fatals fields to this array. function luacheck.process_reports(reports, opts) assert(type(reports) == "table", ("bad argument #1 to 'luacheck.process_reports' (table expected, got %s)"):format(type(reports))) validate_options("luacheck.process_reports", reports, opts) local report = filter.filter(reports, opts) report.warnings = 0 report.errors = 0 report.fatals = 0 for _, file_report in ipairs(report) do if file_report.fatal then report.fatals = report.fatals + 1 else for _, event in ipairs(file_report) do if event.code:sub(1, 1) == "0" then report.errors = report.errors + 1 else report.warnings = report.warnings + 1 end end end end return report end -- Checks strings with options, returns report. -- Tables with .fatal field are unchanged. function luacheck.check_strings(srcs, opts) assert(type(srcs) == "table", ("bad argument #1 to 'luacheck.check_strings' (table expected, got %s)"):format(type(srcs))) for _, item in ipairs(srcs) do assert(type(item) == "string" or type(item) == "table", ( "bad argument #1 to 'luacheck.check_strings' (array of strings or tables expected, got %s)"):format(type(item)) ) end validate_options("luacheck.check_strings", srcs, opts) local reports = {} for i, src in ipairs(srcs) do if type(src) == "table" and src.fatal then reports[i] = src else reports[i] = luacheck.get_report(src) end end return luacheck.process_reports(reports, opts) end function luacheck.check_files(files, opts) assert(type(files) == "table", ("bad argument #1 to 'luacheck.check_files' (table expected, got %s)"):format(type(files))) for _, item in ipairs(files) do assert(type(item) == "string" or io.type(item) == "file", ( "bad argument #1 to 'luacheck.check_files' (array of paths or file handles expected, got %s)"):format(type(item)) ) end validate_options("luacheck.check_files", files, opts) local srcs = {} for i, file in ipairs(files) do srcs[i] = utils.read_file(file) or {fatal = "I/O"} end return luacheck.check_strings(srcs, opts) end function luacheck.get_message(issue) assert(type(issue) == "table", ("bad argument #1 to 'luacheck.get_message' (table expected, got %s)"):format(type(issue))) return format.get_message(issue) end setmetatable(luacheck, {__call = function(_, ...) return luacheck.check_files(...) end}) return luacheck luacheck-0.13.0/bin/0000755000175000017500000000000012642521554013162 5ustar vsevavsevaluacheck-0.13.0/bin/luacheck.bat0000644000175000017500000000005212642521554015426 0ustar vsevavseva@echo off lua.exe "%~dp0\luacheck.lua" %* luacheck-0.13.0/bin/luacheck.lua0000755000175000017500000000005312642521554015445 0ustar vsevavseva#!/usr/bin/env lua require "luacheck.main" luacheck-0.13.0/install.lua0000755000175000017500000000753712642521554014602 0ustar vsevavseva#!/usr/bin/env lua local dirsep = package.config:sub(1, 1) local is_windows = dirsep == "\\" package.path = "src" .. dirsep .. "?.lua" local has_luacheck, luacheck = pcall(require, "luacheck.init") assert(has_luacheck, "couldn't find luacheck module") local has_argparse, argparse = pcall(require, "luacheck.argparse") assert(has_argparse, "couldn't find argparse module") local lua_executable = assert(arg[-1], "couldn't detect Lua executable") local parser = argparse(" install.lua", "Luacheck " .. luacheck._VERSION .. " installer.") parser:argument("path", ([[ Installation path. Luacheck executable scripts will be installed into %sbin. Luacheck modules will be installed into %ssrc. Pass . to build luacheck executable script without installing.]]):format(dirsep, dirsep)) parser:option("--lua", "Absolute path to lua interpreter or its name if it's in PATH.", lua_executable) local args = parser:parse() local function run_command(cmd) if is_windows then cmd = cmd .. " >NUL" else cmd = cmd .. " >/dev/null" end print(" Running " .. cmd) local ok = os.execute(cmd) assert(ok == true or ok == 0, "couldn't run " .. cmd) end local function mkdir(dir) if is_windows then run_command(([[if not exist "%s" md "%s"]]):format(dir, dir)) else run_command(([[mkdir -p "%s"]]):format(dir)) end end local function copy(src, dest) if is_windows then run_command(([[copy /y "%s" "%s"]]):format(src, dest)) else run_command(([[cp "%s" "%s"]]):format(src, dest)) end end print(("Installing luacheck %s into %s"):format(luacheck._VERSION, args.path)) print() local luacheck_executable = "bin" .. dirsep .. "luacheck" local luacheck_src_dir = args.path .. dirsep .. "src" local luacheck_lib_dir = luacheck_src_dir .. dirsep .. "luacheck" local luacheck_bin_dir = args.path .. dirsep .. "bin" if is_windows then print(" Detected Windows environment") luacheck_executable = luacheck_executable .. ".bat" else -- Close enough. print(" Detected POSIX environment") end print(" Writing luacheck executable to " .. luacheck_executable) local fh = assert(io.open(luacheck_executable, "wb"), "couldn't open " .. luacheck_executable) if is_windows then fh:write(([=[ @echo off "%s" -e "package.path=[[%%~dp0..\src\?.lua;%%~dp0..\src\?\init.lua;]]..package.path" "%%~dp0luacheck.lua" %%* ]=]):format(args.lua)) else fh:write(([=[ #!/bin/sh exec "%s" -e "package.path=[[$(dirname "$0")/../src/?.lua;$(dirname "$0")/../src/?/init.lua;]]..package.path" "$(dirname "$0")/luacheck.lua" "$@" ]=]):format(args.lua)) end fh:close() if not is_windows then run_command(([[chmod +x "%s"]]):format(luacheck_executable)) end if args.path == "." then print() print(("Built luacheck %s executable script (%s)."):format(luacheck._VERSION, luacheck_executable)) return end print(" Installing luacheck modules into " .. luacheck_src_dir) mkdir(luacheck_lib_dir) for _, filename in ipairs { "main.lua", "init.lua", "config.lua", "linearize.lua", "analyze.lua", "reachability.lua", "core_utils.lua", "check.lua", "parser.lua", "lexer.lua", "filter.lua", "options.lua", "inline_options.lua", "stds.lua", "expand_rockspec.lua", "multithreading.lua", "cache.lua", "format.lua", "version.lua", "fs.lua", "globbing.lua", "utils.lua", "argparse.lua"} do copy("src" .. dirsep .. "luacheck" .. dirsep .. filename, luacheck_lib_dir) end print(" Installing luacheck executables into " .. luacheck_bin_dir) mkdir(luacheck_bin_dir) copy(luacheck_executable, luacheck_bin_dir) copy("bin" .. dirsep .. "luacheck.lua", luacheck_bin_dir) print() print(("Installed luacheck %s into %s."):format(luacheck._VERSION, args.path)) print(("Please ensure that %s is in PATH."):format(luacheck_bin_dir)) luacheck-0.13.0/LICENSE0000644000175000017500000000210212642521554013412 0ustar vsevavsevaThe MIT License (MIT) Copyright (c) 2014 - 2016 Peter Melnichenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.