diff -Nru luacheck-0.22.0/appveyor.yml luacheck-0.23.0/appveyor.yml --- luacheck-0.22.0/appveyor.yml 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/appveyor.yml 2018-09-18 19:43:27.000000000 +0000 @@ -10,7 +10,7 @@ - LUA: "luajit 2.0" - LUA: "luajit 2.1" -build_script: +before_build: - set PATH=C:\Python27\Scripts;%PATH% - pip install hererocks - pip install codecov @@ -18,6 +18,11 @@ - call here\bin\activate - luarocks install busted - luarocks install cluacov + - luarocks install luautf8 + - luarocks install luasocket + +build_script: + - luarocks make test_script: busted -c diff -Nru luacheck-0.22.0/.busted luacheck-0.23.0/.busted --- luacheck-0.22.0/.busted 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/.busted 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,5 @@ +return { + _all = { + ["exclude-pattern"] = "sample_spec" + } +} diff -Nru luacheck-0.22.0/CHANGELOG.md luacheck-0.23.0/CHANGELOG.md --- luacheck-0.22.0/CHANGELOG.md 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/CHANGELOG.md 2018-09-18 19:43:27.000000000 +0000 @@ -1,5 +1,61 @@ # Changelog +## 0.23.0 (2018-09-18) + +### Breaking changes + +* Removed `--no-inline` CLI option and `inline` config option, inline options + are now always enabled. +* Inline comments are now supposed to be only in short comments + but not long ones. +* Installer script (install.lua) is removed. Luacheck can still be installed + manually by recursively copying `src/*` to a directory in `package.path` + and copying `bin/luacheck.lua` to a directory in `PATH` as `luacheck`. + +### New features and improvements + +* Warning columns are now reported in Unicode codepoints if input is + valid UTF-8 (#45). +* Added indentaion-based guessing of a better location for missing `end` + and `until` syntax errors. +* Added `luacheckrc` set of allowed globals containing globals used in + Luacheck config to set options. +* Added default stds equivalent to predefined per-path std overrides + in config: + - `files["**/spec/**/*_spec.lua"].std = "+busted"`; + - `files["**/test/**/*_spec.lua"].std = "+busted"`; + - `files["**/tests/**/*_spec.lua"].std = "+busted"`; + - `files["**/*.rockspec"].std = "+rockspec"`; + - `files["**/*.luacheckrc"].std = "+luacheckrc"`. +* Added detection of numeric for loops going from `#t` to `1` without + negative step (#160). +* Added support for LuaRocks 3 module autodetection when checking + rockspecs (#176). +* Updated `love` standard for LÖVE 11.1 (#178). + +### Changes + +* Default set of standard globals is now always `max`, allowing globals of all + Lua versions. `_G` std is deprecated. + +### Fixes + +* Added missing globals to `rockspec` std: `hooks`, `deploy`, + `build_dependencies`, `test_dependencies`, and `test`. +* Fixed line lengths appearing in the output before other warnings on the same + line even if their column numbers are smaller. + +### Miscellaneous + +* Luacheck now depends on argparse instead of bundling it. +* LuaFileSystem dependency is now required. + +## 0.22.1 (2018-07-01) + +### Improvements + +* Reduced amount of RAM used when checking a large number of files. + ## 0.22.0 (2018-05-09) ### New features and improvements @@ -17,8 +73,9 @@ redefined label errors point to the previous definition, unpaired tokens such as `function`/`end` point to the the first token (#134). * `luacheck` module now adds `prev_end_column` field to warning events that - already have `prev_line` and `prev_column` fields, and `overwritten_end_column` - for warnings with `overwritten_line` and `overwritten_column`. + already have `prev_line` and `prev_column` fields, and + `overwritten_end_column` for warnings with `overwritten_line` and + `overwritten_column`. * Improved error messages for invalid options and config: when an option is invalid, extra context is provided instead of just the name. * Custom stds are now validated on config load. diff -Nru luacheck-0.22.0/debian/changelog luacheck-0.23.0/debian/changelog --- luacheck-0.22.0/debian/changelog 2018-06-21 09:12:00.000000000 +0000 +++ luacheck-0.23.0/debian/changelog 2018-10-19 16:04:33.000000000 +0000 @@ -1,3 +1,13 @@ +luacheck (0.23.0-1) unstable; urgency=medium + + * New upstream version 0.23.0 + * add new source dir + * wrap-and-sort -sat + * update standards-version no changes needed + * add argparse as depends + + -- Victor Seva Fri, 19 Oct 2018 18:04:33 +0200 + luacheck (0.22.0-1) unstable; urgency=medium * update Vcs-* urls to salsa diff -Nru luacheck-0.22.0/debian/control luacheck-0.23.0/debian/control --- luacheck-0.22.0/debian/control 2018-06-21 09:12:00.000000000 +0000 +++ luacheck-0.23.0/debian/control 2018-10-19 16:04:33.000000000 +0000 @@ -2,22 +2,27 @@ Section: interpreters Priority: optional Maintainer: Victor Seva -Uploaders: Enrico Tassi -Build-Depends: debhelper (>= 9~), - dh-lua (>= 16~), - python3-sphinx -Standards-Version: 4.1.4 +Uploaders: + Enrico Tassi , +Build-Depends: + debhelper (>= 9~), + dh-lua (>= 16~), + python3-sphinx, +Standards-Version: 4.2.1 Vcs-Git: https://salsa.debian.org/lua-team/lua-check.git Vcs-Browser: https://salsa.debian.org/lua-team/lua-check Homepage: https://github.com/mpeterv/luacheck Package: lua-check Architecture: all -Depends: lua5.1, - lua-filesystem, - ${misc:Depends}, - ${shlibs:Depends} -Provides: ${lua:Provides} +Depends: + lua-argparse, + lua-filesystem, + lua5.1, + ${misc:Depends}, + ${shlibs:Depends}, +Provides: + ${lua:Provides}, XB-Lua-Versions: ${lua:Versions} Description: static analyzer and a linter for the Lua language Luacheck is a static analyzer and a linter for Lua which diff -Nru luacheck-0.22.0/debian/lua5.1.dh-lua.conf luacheck-0.23.0/debian/lua5.1.dh-lua.conf --- luacheck-0.22.0/debian/lua5.1.dh-lua.conf 2018-06-21 09:12:00.000000000 +0000 +++ luacheck-0.23.0/debian/lua5.1.dh-lua.conf 2018-10-19 16:04:33.000000000 +0000 @@ -1,5 +1,5 @@ PKG_NAME=check -LUA_SOURCES=src/luacheck/* +LUA_SOURCES=$(wildcard src/luacheck/*.lua) $(wildcard src/luacheck/stages/*.lua) LUA_SOURCES_MANGLER=sed 's?^src/??' LUA_MODNAME=luacheck diff -Nru luacheck-0.22.0/docsrc/cli.rst luacheck-0.23.0/docsrc/cli.rst --- luacheck-0.22.0/docsrc/cli.rst 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/cli.rst 2018-09-18 19:43:27.000000000 +0000 @@ -68,8 +68,10 @@ See :ref:`secondaryvaluesandvariables` ``--no-self`` Filter out warnings related to implicit ``self`` argument. -``--std `` Set standard globals. ```` can be one of: +``--std `` Set standard globals, default is ``max``. ```` can be one of: + * ``max`` - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x; + * ``min`` - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x; * ``lua51`` - globals of Lua 5.1 without deprecated ones; * ``lua51c`` - globals of Lua 5.1; * ``lua52`` - globals of Lua 5.2; @@ -78,13 +80,10 @@ * ``lua53c`` - globals of Lua 5.3 compiled with LUA_COMPAT_5_2; * ``luajit`` - globals of LuaJIT 2.x; * ``ngx_lua`` - globals of Openresty `lua-nginx-module `_ 0.10.10, including standard LuaJIT 2.x globals; - * ``min`` - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x; - * ``max`` - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x; - * ``_G`` (default) - same as ``lua51c``, ``lua52c``, ``lua53c``, or ``luajit`` depending on version of Lua used - to run ``luacheck`` or same as ``max`` if couldn't detect the version; - * ``love`` - globals added by `LÖVE `_ (love2d); - * ``busted`` - globals added by Busted 2.0; - * ``rockspec`` - globals allowed in rockspecs; + * ``love`` - globals added by `LÖVE `_; + * ``busted`` - globals added by Busted 2.0, by default added for files ending with ``_spec.lua`` within ``spec``, ``test``, and ``tests`` subdirectories; + * ``rockspec`` - globals allowed in rockspecs, by default added for files ending with ``.rockspec``; + * ``luacheckrc`` - globals allowed in Luacheck configs, by default added for files ending with ``.luacheckrc``; * ``none`` - no standard globals. See :ref:`stds` @@ -116,7 +115,6 @@ ``--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. ``--default-config `` Default path to custom configuration file, to be used if ``--[no-]config`` is not used and ``.luacheckrc`` is not found. @@ -182,7 +180,7 @@ Sets of standard globals ------------------------ -CLI option ``--stds`` allows combining built-in sets described above using ``+``. For example, ``--std max`` is equivalent to ``--std=lua51c+lua52c+lua53c+luajit``. Leading plus sign adds new sets to current 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` +CLI option ``--stds`` allows combining built-in sets described above using ``+``. For example, ``--std max`` is equivalent to ``--std=lua51c+lua52c+lua53c+luajit``. Leading plus sign adds new sets to current one instead of replacing it. For instance, ``--std +love`` is suitable for checking files using `LÖVE `_ framework. Custom sets of globals can be defined by mutating global variable ``stds`` in config. See :ref:`custom_stds` Formatters ---------- diff -Nru luacheck-0.22.0/docsrc/config.rst luacheck-0.23.0/docsrc/config.rst --- luacheck-0.22.0/docsrc/config.rst 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/config.rst 2018-09-18 19:43:27.000000000 +0000 @@ -36,7 +36,7 @@ ``unused_args`` Boolean ``true`` ``unused_secondaries`` Boolean ``true`` ``self`` Boolean ``true`` -``std`` String or set of standard globals ``"_G"`` +``std`` String or set of standard globals ``"max"`` ``globals`` Array of strings or field definition map ``{}`` ``new_globals`` Array of strings or field definition map (Do not overwrite) ``read_globals`` Array of strings or field definition map ``{}`` @@ -54,7 +54,6 @@ ``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: @@ -153,7 +152,7 @@ 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[]`` if ```` matches ````, applying entries for more general globs first. For example, the following config re-enables detection of unused arguments only for files in ``src/dir``, but not for files ending with ``_special.lua``, and allows using `Busted `_ globals within ``spec/``: +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[]`` if ```` matches ````, applying entries for more general globs first. For example, the following config re-enables detection of unused arguments only for files in ``src/dir``, but not for files ending with ``_special.lua``: .. code-block:: lua :linenos: @@ -162,7 +161,6 @@ ignore = {"212"} files["src/dir"] = {enable = {"212"}} files["src/dir/**/*_special.lua"] = {ignore = {"212"}} - files["spec"] = {std = "+busted"} Note that ``files`` table supports autovivification, so that @@ -177,3 +175,19 @@ files["src/dir"] = {enable = {"212"}} are equivalent. + +Default per-path std overrides +------------------------------ + +``luacheck`` uses a set of default per-path overrides: + +.. code-block:: lua + :linenos: + + files["**/spec/**/*_spec.lua"].std = "+busted" + files["**/test/**/*_spec.lua"].std = "+busted" + files["**/tests/**/*_spec.lua"].std = "+busted" + files["**/*.rockspec"].std = "+rockspec" + files["**/*.luacheckrc"].std = "+luacheckrc" + +Each of these can be overriden by setting a different ``std`` value for the corresponding key in ``files``. diff -Nru luacheck-0.22.0/docsrc/conf.py luacheck-0.23.0/docsrc/conf.py --- luacheck-0.22.0/docsrc/conf.py 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/conf.py 2018-09-18 19:43:27.000000000 +0000 @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.22.0' +version = '0.23.0' # The full version, including alpha/beta/rc tags. -release = '0.22.0' +release = '0.23.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -Nru luacheck-0.22.0/docsrc/index.rst luacheck-0.23.0/docsrc/index.rst --- luacheck-0.22.0/docsrc/index.rst 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/index.rst 2018-09-18 19:43:27.000000000 +0000 @@ -11,4 +11,4 @@ inline module -This is documentation for 0.22.0 version of `Luacheck `_, a linter for `Lua `_. +This is documentation for 0.23.0 version of `Luacheck `_, a linter for `Lua `_. diff -Nru luacheck-0.22.0/docsrc/inline.rst luacheck-0.23.0/docsrc/inline.rst --- luacheck-0.22.0/docsrc/inline.rst 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/inline.rst 2018-09-18 19:43:27.000000000 +0000 @@ -3,7 +3,7 @@ Luacheck supports setting some options directly in the checked files using inline configuration comments. These inline options have the highest priority, overwriting both config options and CLI options. -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. It can also contain notes enclosed in balanced parentheses, which are ignored. The following options are supported: +An inline configuration comment is a short comment starting 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. It can also contain notes enclosed in balanced parentheses, which are ignored. The following options are supported: ======================= ==================================================================== Option Number of arguments @@ -60,5 +60,3 @@ foo() -- No warning. -- luacheck: pop foo() -- Warning is emitted. - -Inline options can be completely disabled using ``--no-inline`` CLI option or ``inline`` config option. diff -Nru luacheck-0.22.0/docsrc/warnings.rst luacheck-0.23.0/docsrc/warnings.rst --- luacheck-0.22.0/docsrc/warnings.rst 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/docsrc/warnings.rst 2018-09-18 19:43:27.000000000 +0000 @@ -3,9 +3,9 @@ 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; unlike warnings, they can not be ignored. -==== ================================================================= +==== ============================================================================= Code Description -==== ================================================================= +==== ============================================================================= 011 A syntax error. 021 An invalid inline option. 022 An unpaired inline push directive. @@ -51,13 +51,14 @@ 542 An empty ``if`` branch. 551 An empty statement. 561 Cyclomatic complexity of a function is too high. +571 A numeric for loop goes from #(expr) down to 1 or less without negative step. 611 A line consists of nothing but whitespace. 612 A line contains trailing whitespace. 613 Trailing whitespace in a string. 614 Trailing whitespace in a comment. 621 Inconsistent indentation (``SPACE`` followed by ``TAB``). 631 Line is too long. -==== ================================================================= +==== ============================================================================= Global variables (1xx) ---------------------- @@ -207,6 +208,26 @@ If a limit is set using ``--max-cyclomatic-complexity`` CLI option or corresponding config or inline options, Luacheck warns about functions with too high cyclomatic complexity. +Reversed numeric for loops +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Iterating a table in reverse using a numeric for loop going from ``#t`` to ``1`` requires a negative loop step. Luacheck warns about loops +going from ``#(some expression)`` to ``1`` or a smaller constant when the loop step is not negative: + +.. code-block:: lua + :linenos: + + -- Warning for this loop: + -- numeric for loop goes from #(expr) down to 1 but loop step is not negative + for i = #t, 1 do + print(t[i]) + end + + -- This loop is okay. + for i = #t, 1, -1 do + print(t[i]) + end + Formatting issues (6xx) ----------------------- diff -Nru luacheck-0.22.0/.gitignore luacheck-0.23.0/.gitignore --- luacheck-0.22.0/.gitignore 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/.gitignore 2018-09-18 19:43:27.000000000 +0000 @@ -1,7 +1,8 @@ -bin/luacheck .luacheckcache luacov.stats.out luacov.report.out doc build package +scripts/UnicodeData-* +docsrc/.doctrees diff -Nru luacheck-0.22.0/install.lua luacheck-0.23.0/install.lua --- luacheck-0.22.0/install.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/install.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,151 +0,0 @@ -#!/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) -parser:option("--destdir", "Path to stage luacheck installation into") - -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 args.destdir then - dir = args.destdir .. dirsep .. dir - end - 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 args.destdir then - dest = args.destdir .. dirsep .. dest - end - 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=[[%s/?.lua;%s/?/init.lua;]]..package.path" "%s/luacheck.lua" "$@" -]=]):format(args.lua, luacheck_src_dir, luacheck_src_dir, '$(dirname "$0")')) -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({ - "init.lua", - "argparse.lua", - "builtin_standards.lua", - "cache.lua", - "check.lua", - "config.lua", - "core_utils.lua", - "detect_bad_whitespace.lua", - "detect_cyclomatic_complexity.lua", - "detect_globals.lua", - "detect_uninit_access.lua", - "detect_unreachable_code.lua", - "detect_unused_locals.lua", - "detect_unused_rec_funcs.lua", - "expand_rockspec.lua", - "filter.lua", - "format.lua", - "fs.lua", - "globbing.lua", - "inline_options.lua", - "lexer.lua", - "lfs_fs.lua", - "linearize.lua", - "love_standard.lua", - "lua_fs.lua", - "main.lua", - "name_functions.lua", - "multithreading.lua", - "ngx_standard.lua", - "options.lua", - "parser.lua", - "resolve_locals.lua", - "runner.lua", - "standards.lua", - "utils.lua", - "version.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)) diff -Nru luacheck-0.22.0/luacheck-dev-1.rockspec luacheck-0.23.0/luacheck-dev-1.rockspec --- luacheck-0.22.0/luacheck-dev-1.rockspec 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/luacheck-dev-1.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -6,52 +6,64 @@ 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. +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", + "argparse >= 0.6.0", "luafilesystem >= 1.6.3" } build = { type = "builtin", modules = { luacheck = "src/luacheck/init.lua", - ["luacheck.argparse"] = "src/luacheck/argparse.lua", ["luacheck.builtin_standards"] = "src/luacheck/builtin_standards.lua", ["luacheck.cache"] = "src/luacheck/cache.lua", ["luacheck.check"] = "src/luacheck/check.lua", + ["luacheck.check_state"] = "src/luacheck/check_state.lua", ["luacheck.config"] = "src/luacheck/config.lua", ["luacheck.core_utils"] = "src/luacheck/core_utils.lua", - ["luacheck.detect_bad_whitespace"] = "src/luacheck/detect_bad_whitespace.lua", - ["luacheck.detect_cyclomatic_complexity"] = "src/luacheck/detect_cyclomatic_complexity.lua", - ["luacheck.detect_globals"] = "src/luacheck/detect_globals.lua", - ["luacheck.detect_uninit_access"] = "src/luacheck/detect_uninit_access.lua", - ["luacheck.detect_unreachable_code"] = "src/luacheck/detect_unreachable_code.lua", - ["luacheck.detect_unused_locals"] = "src/luacheck/detect_unused_locals.lua", - ["luacheck.detect_unused_rec_funcs"] = "src/luacheck/detect_unused_rec_funcs.lua", + ["luacheck.decoder"] = "src/luacheck/decoder.lua", ["luacheck.expand_rockspec"] = "src/luacheck/expand_rockspec.lua", ["luacheck.filter"] = "src/luacheck/filter.lua", ["luacheck.format"] = "src/luacheck/format.lua", ["luacheck.fs"] = "src/luacheck/fs.lua", ["luacheck.globbing"] = "src/luacheck/globbing.lua", - ["luacheck.inline_options"] = "src/luacheck/inline_options.lua", ["luacheck.lexer"] = "src/luacheck/lexer.lua", - ["luacheck.lfs_fs"] = "src/luacheck/lfs_fs.lua", - ["luacheck.linearize"] = "src/luacheck/linearize.lua", ["luacheck.love_standard"] = "src/luacheck/love_standard.lua", - ["luacheck.lua_fs"] = "src/luacheck/lua_fs.lua", ["luacheck.main"] = "src/luacheck/main.lua", - ["luacheck.name_functions"] = "src/luacheck/name_functions.lua", ["luacheck.multithreading"] = "src/luacheck/multithreading.lua", ["luacheck.ngx_standard"] = "src/luacheck/ngx_standard.lua", ["luacheck.options"] = "src/luacheck/options.lua", ["luacheck.parser"] = "src/luacheck/parser.lua", - ["luacheck.resolve_locals"] = "src/luacheck/resolve_locals.lua", + ["luacheck.profiler"] = "src/luacheck/profiler.lua", ["luacheck.runner"] = "src/luacheck/runner.lua", + ["luacheck.stages"] = "src/luacheck/stages.lua", + ["luacheck.stages.detect_bad_whitespace"] = "src/luacheck/stages/detect_bad_whitespace.lua", + ["luacheck.stages.detect_cyclomatic_complexity"] = "src/luacheck/stages/detect_cyclomatic_complexity.lua", + ["luacheck.stages.detect_empty_blocks"] = "src/luacheck/stages/detect_empty_blocks.lua", + ["luacheck.stages.detect_empty_statements"] = "src/luacheck/stages/detect_empty_statements.lua", + ["luacheck.stages.detect_globals"] = "src/luacheck/stages/detect_globals.lua", + ["luacheck.stages.detect_reversed_fornum_loops"] = "src/luacheck/stages/detect_reversed_fornum_loops.lua", + ["luacheck.stages.detect_unbalanced_assignments"] = "src/luacheck/stages/detect_unbalanced_assignments.lua", + ["luacheck.stages.detect_uninit_accesses"] = "src/luacheck/stages/detect_uninit_accesses.lua", + ["luacheck.stages.detect_unreachable_code"] = "src/luacheck/stages/detect_unreachable_code.lua", + ["luacheck.stages.detect_unused_fields"] = "src/luacheck/stages/detect_unused_fields.lua", + ["luacheck.stages.detect_unused_locals"] = "src/luacheck/stages/detect_unused_locals.lua", + ["luacheck.stages.linearize"] = "src/luacheck/stages/linearize.lua", + ["luacheck.stages.name_functions"] = "src/luacheck/stages/name_functions.lua", + ["luacheck.stages.parse"] = "src/luacheck/stages/parse.lua", + ["luacheck.stages.parse_inline_options"] = "src/luacheck/stages/parse_inline_options.lua", + ["luacheck.stages.resolve_locals"] = "src/luacheck/stages/resolve_locals.lua", + ["luacheck.stages.unwrap_parens"] = "src/luacheck/stages/unwrap_parens.lua", ["luacheck.standards"] = "src/luacheck/standards.lua", + ["luacheck.unicode"] = "src/luacheck/unicode.lua", + ["luacheck.unicode_printability_boundaries"] = "src/luacheck/unicode_printability_boundaries.lua", ["luacheck.utils"] = "src/luacheck/utils.lua", ["luacheck.version"] = "src/luacheck/version.lua" }, diff -Nru luacheck-0.22.0/.luacheckrc luacheck-0.23.0/.luacheckrc --- luacheck-0.22.0/.luacheckrc 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/.luacheckrc 2018-09-18 19:43:27.000000000 +0000 @@ -1,6 +1,5 @@ std = "min" cache = true -include_files = {"src", "spec/*.lua", "install.lua"} +include_files = {"src", "spec/*.lua", "scripts/*.lua", "*.rockspec", "*.luacheckrc"} -files["spec/*_spec.lua"].std = "+busted" -files["src/luacheck/argparse.lua"].max_line_length = 140 +files["src/luacheck/unicode_printability_boundaries.lua"].max_line_length = false diff -Nru luacheck-0.22.0/README.md luacheck-0.23.0/README.md --- luacheck-0.22.0/README.md 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/README.md 2018-09-18 19:43:27.000000000 +0000 @@ -34,12 +34,12 @@ luarocks install luacheck ``` -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 (`luarocks install lanes`). +For parallel checking Luacheck additionally requires [LuaLanes](https://github.com/LuaLanes/lanes), which can be installed using LuaRocks as well (`luarocks install lanes`). ### Windows binary download For Windows there is single-file 64-bit binary distribution, bundling Lua 5.3.4, Luacheck, LuaFileSystem, and LuaLanes using [LuaStatic](https://github.com/ers35/luastatic): -[download](https://github.com/mpeterv/luacheck/releases/download/0.22.0/luacheck.exe). +[download](https://github.com/mpeterv/luacheck/releases/download/0.23.0/luacheck.exe). ## Basic usage @@ -107,13 +107,13 @@ ## Development -Luacheck is currently in development. The latest released version is 0.22.0. The interface of the `luacheck` module may change between minor releases. The command line interface is fairly stable. +Luacheck is currently in development. The latest released version is 0.23.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` (using `sudo` if necessary) from its root directory to install dev version of Luacheck. To run Luacheck using sources in current directory without installing it, run `lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' bin/luacheck.lua ...`. To test Luacheck, ensure that you have [busted](http://olivinelabs.com/busted/) installed and run `busted`. +After the Luacheck repo is cloned and changes are made, run `luarocks make` (using `sudo` if necessary) from its root directory to install dev version of Luacheck. To run Luacheck using sources in current directory without installing it, run `lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' bin/luacheck.lua ...`. To test Luacheck, ensure that you have [busted](http://olivinelabs.com/busted/) and [luautf8](https://github.com/starwing/luautf8) installed and run `busted`. ## License diff -Nru luacheck-0.22.0/scripts/dedicated_coverage.sh luacheck-0.23.0/scripts/dedicated_coverage.sh --- luacheck-0.22.0/scripts/dedicated_coverage.sh 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/scripts/dedicated_coverage.sh 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -eu +set -o pipefail + +# Collects test coverage for luacheck modules with associated spec files. +# Runs spec files from the arguments or all spec files. +# Each module can be covered only from its own spec file. +# Should be executed from root Luacheck directory. + +declare -A spec_to_module +spec_to_module[spec/bad_whitespace_spec.lua]=src/luacheck/stages/detect_bad_whitespace.lua +spec_to_module[spec/cache_spec.lua]=src/luacheck/cache.lua +spec_to_module[spec/check_spec.lua]=src/luacheck/check.lua +spec_to_module[spec/config_spec.lua]=src/luacheck/config.lua +spec_to_module[spec/decoder_spec.lua]=src/luacheck/decoder.lua +spec_to_module[spec/empty_blocks_spec.lua]="src/luacheck/stages/detect_empty_blocks.lua" +spec_to_module[spec/expand_rockspec_spec.lua]=src/luacheck/expand_rockspec.lua +spec_to_module[spec/filter_spec.lua]=src/luacheck/filter.lua +spec_to_module[spec/format_spec.lua]=src/luacheck/format.lua +spec_to_module[spec/fs_spec.lua]=src/luacheck/fs.lua +spec_to_module[spec/globbing_spec.lua]=src/luacheck/globbing.lua +spec_to_module[spec/luacheck_spec.lua]=src/luacheck/init.lua +spec_to_module[spec/lexer_spec.lua]=src/luacheck/lexer.lua +spec_to_module[spec/cli_spec.lua]=src/luacheck/main.lua +spec_to_module[spec/options_spec.lua]=src/luacheck/options.lua +spec_to_module[spec/parser_spec.lua]=src/luacheck/parser.lua +spec_to_module[spec/cyclomatic_complexity_spec.lua]=src/luacheck/stages/detect_cyclomatic_complexity.lua +spec_to_module[spec/globals_spec.lua]=src/luacheck/stages/detect_globals.lua +spec_to_module[spec/reversed_fornum_loops_spec.lua]=src/luacheck/stages/detect_reversed_fornum_loops.lua +spec_to_module[spec/unbalanced_assignments_spec.lua]=src/luacheck/stages/detect_unbalanced_assignments.lua +spec_to_module[spec/uninit_accesses_spec.lua]=src/luacheck/stages/detect_uninit_accesses.lua +spec_to_module[spec/unreachable_code_spec.lua]=src/luacheck/stages/detect_unreachable_code.lua +spec_to_module[spec/unused_fields_spec.lua]=src/luacheck/stages/detect_unused_fields.lua +spec_to_module[spec/unused_locals_spec.lua]=src/luacheck/stages/detect_unused_locals.lua +spec_to_module[spec/linearize_spec.lua]=src/luacheck/stages/linearize.lua +spec_to_module[spec/resolve_locals_spec.lua]=src/luacheck/stages/resolve_locals.lua +spec_to_module[spec/standards_spec.lua]=src/luacheck/standards.lua +spec_to_module[spec/utils_spec.lua]=src/luacheck/utils.lua + +if [ $# -eq 0 ]; then + specs="$(sort <<< "${!spec_to_module[@]}")" +else + specs="$@" +fi + +{ + echo Spec Module Hits Missed Coverage + + for spec in $specs; do + if [ -v spec_to_module[$spec] ]; then + module="${spec_to_module[$spec]}" + + rm -f luacov.stats.out + rm -f luacov.report.out + + echo "busted -c $spec" >&2 + busted -c "$spec" >&2 || true + luacov + echo -n "$spec " + grep -P "$module +[^ ]+ +[^ ]+ +[^ ]+" luacov.report.out || echo "$module 0 0 0.00%" + echo >&2 + else + echo "No associated module for spec $spec" >&2 + fi + done +} | column -t diff -Nru luacheck-0.22.0/scripts/gen_unicode_printability_module.sh luacheck-0.23.0/scripts/gen_unicode_printability_module.sh --- luacheck-0.22.0/scripts/gen_unicode_printability_module.sh 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/scripts/gen_unicode_printability_module.sh 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -eu +set -o pipefail + +# Generates luacheck.unicode_printability_boundaries module given Unicode version. +# Should be executed from root Luacheck directory. + +url="https://www.unicode.org/Public/$1/ucd/UnicodeData.txt" +cache="scripts/UnicodeData-$1.txt" + +if [ ! -e "$cache" ]; then + wget -O "$cache" "$url" +fi + +( + echo "-- Autogenerated using data from $url"; + lua scripts/unicode_data_to_printability_module.lua < "$cache" +) > src/luacheck/unicode_printability_boundaries.lua diff -Nru luacheck-0.22.0/scripts/unicode_data_to_printability_module.lua luacheck-0.23.0/scripts/unicode_data_to_printability_module.lua --- luacheck-0.22.0/scripts/unicode_data_to_printability_module.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/scripts/unicode_data_to_printability_module.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,86 @@ +-- Reads Unicode character data in UnicodeData.txt format from stdin. +-- Prints a Lua module retuning an array of first codepoints of +-- each continuous block of codepoints that are all printable or all not printable. +-- See https://unicode.org/reports/tr44/ + +local category_printabilities = { + Lu = true, + Ll = true, + Lt = true, + Lm = true, + Lo = true, + Mn = true, + Mc = true, + Me = true, + Nd = true, + Nl = true, + No = true, + Pc = true, + Pd = true, + Ps = true, + Pe = true, + Pi = true, + Pf = true, + Po = true, + Sm = true, + Sc = true, + Sk = true, + So = true, + Zs = true, + Zl = false, + Zp = false, + Cc = false, + Cf = false, + Cs = false, + Co = false, + Cn = false +} + +local codepoint_printabilities = {} +local max_codepoint = 0 + +local range_start_codepoint + +for line in io.lines() do + local codepoint_hex, name, category = assert(line:match("^([^;]+);([^;]+);([^;]+)")) + local codepoint = assert(tonumber("0x" .. codepoint_hex)) + local printability = category_printabilities[category] + assert(printability ~= nil) + + if name:find(", First>$") then + assert(not range_start_codepoint) + range_start_codepoint = codepoint + elseif name:find(", Last>$") then + assert(range_start_codepoint and range_start_codepoint >= range_start_codepoint) + + for range_codepoint = range_start_codepoint, codepoint do + codepoint_printabilities[range_codepoint] = printability + end + + range_start_codepoint = nil + else + codepoint_printabilities[codepoint] = printability + end + + max_codepoint = math.max(max_codepoint, codepoint) +end + +assert(not range_start_codepoint) + +local parts = {"return {"} +local prev_printability = true + +-- Iterate up to a non-existent codepoint to ensure that the last required codepoint is printed. +for codepoint = 0, max_codepoint + 1 do + local printability = codepoint_printabilities[codepoint] or false + + if printability ~= prev_printability then + table.insert(parts, ("%d,"):format(codepoint)) + end + + prev_printability = printability +end + +table.insert(parts, "}") +print(table.concat(parts)) + diff -Nru luacheck-0.22.0/spec/bad_whitespace_spec.lua luacheck-0.23.0/spec/bad_whitespace_spec.lua --- luacheck-0.22.0/spec/bad_whitespace_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/bad_whitespace_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,74 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_bad_whitespace", src)) +end + +describe("bad whitespace detection", function() + it("detects lines with only whitespace", function() + assert_warnings({ + {code = "611", line = 1, column = 1, end_column = 4}, + {code = "611", line = 3, column = 1, end_column = 1} + }, " \n--[[\n \n]]\n") + end) + + it("detects trailing whitespace with different warnings code depending on line ending type", function() + assert_warnings({ + {code = "612", line = 1, column = 8, end_column = 9}, + {code = "613", line = 2, column = 13, end_column = 13}, + {code = "612", line = 3, column = 8, end_column = 8}, + {code = "614", line = 4, column = 11, end_column = 14} + }, "local a \nlocal b = [[ \nthing]] \nlocal c --\t\t\t\t\nlocal d\n") + end) + + it("detects spaces followed by tabs", function() + assert_warnings({ + {code = "621", line = 1, column = 1, end_column = 5} + }, " \t \tlocal foo\n\t\t local bar\n") + end) + + it("does not warn on spaces followed by tabs if the line has only whitespace", function() + assert_warnings({ + {code = "611", line = 1, column = 1, end_column = 7} + }, " \t \t \n") + end) + + it("can detect both trailing whitespace and inconsistent indentation on the same line", function() + assert_warnings({ + {code = "621", line = 1, column = 1, end_column = 2}, + {code = "612", line = 1, column = 10, end_column = 10} + }, " \tlocal a \n") + end) + + it("handles lack of trailing newline", function() + assert_warnings({ + {code = "611", line = 2, column = 1, end_column = 5} + }, "local a\n ") + + assert_warnings({ + {code = "612", line = 2, column = 8, end_column = 12} + }, "local a\nlocal b ") + + assert_warnings({ + {code = "621", line = 1, column = 1, end_column = 2}, + {code = "614", line = 1, column = 13, end_column = 16} + }, " \tlocal a -- ") + end) + + it("provides correct column ranges in presence of two-byte line endings", function() + assert_warnings({ + {code = "612", line = 1, column = 10, end_column = 13}, + {code = "621", line = 2, column = 1, end_column = 4}, + {code = "611", line = 3, column = 1, end_column = 3} + }, "local foo \r\n \tlocal bar\n\r ") + end) + + it("provides correct column ranges in presence of utf8", function() + assert_warnings({ + {code = "612", line = 1, column = 17, end_column = 20}, + {code = "611", line = 2, column = 1, end_column = 4}, + {code = "621", line = 3, column = 1, end_column = 4}, + {code = "614", line = 3, column = 20, end_column = 24}, + }, "local foo = '\204\128\204\130' \n \n \tlocal bar -- \240\144\128\128\224\166\152 \n") + end) +end) diff -Nru luacheck-0.22.0/spec/cache_spec.lua luacheck-0.23.0/spec/cache_spec.lua --- luacheck-0.22.0/spec/cache_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/cache_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -18,85 +18,85 @@ it("returns serialized result", function() assert.same( - [[return {{{"111","foo",5,100,102,[24]={"faa"}},{"211","bar",4,1,3,[9]=true},{"011",[4]=100000,[13]="near '\"'"}},{}}]], + [[return {{{"111",5,100,102,"foo",{"faa"}},{"211",4,1,3,"bar",nil,true},{"011",nil,100000,nil,"near '\"'"}},{}}]], cache.serialize({ - events = { + warnings = { {code = "111", name = "foo", indexing = {"faa"}, line = 5, column = 100, end_column = 102}, {code = "211", name = "bar", line = 4, column = 1, end_column = 3, secondary = true}, {code = "011", column = 100000, msg = "near '\"'"} }, - per_line_options = {} + inline_options = {} }) ) end) it("puts repeating string values into locals", function() assert.same( - [[local A,B="111","foo";return {{{A,B,5,100},{A,B,6,100,[9]=true},{"011",[4]=100000,[13]="near '\"'"}},{},{}}]], + [[local A,B="111","foo";return {{{A,5,100,nil,B},{A,6,100,nil,B},{"011",nil,100000,nil,"near '\"'"}},{},{}}]], cache.serialize({ - events = { + warnings = { {code = "111", name = "foo", line = 5, column = 100}, {code = "111", name = "foo", line = 6, column = 100, secondary = true}, {code = "011", column = 100000, msg = "near '\"'"} }, - per_line_options = {}, + inline_options = {}, line_lengths = {} }) ) 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"}},{},{}}', + local warnings = {} + local expected_parts1 = {"local A"} + local expected_parts2 = {'="111"'} + local expected_parts3 = {";return {{"} + + local function add_char(b) + local c = string.char(b) + table.insert(warnings, {code = "111", name = c}) + table.insert(warnings, {code = "111", name = c}) + table.insert(expected_parts1, "," .. c) + table.insert(expected_parts2, ',"' .. c .. '"') + table.insert(expected_parts3, ('{A,nil,nil,nil,%s},{A,nil,nil,nil,%s},'):format(c, c)) + end + + local function add_extra(name) + table.insert(warnings, {code = "111", name = name}) + table.insert(warnings, {code = "111", name = name}) + table.insert(expected_parts3, ('{A,nil,nil,nil,"%s"},{A,nil,nil,nil,"%s"},'):format(name, name)) + end + + for b = ("B"):byte(), ("Z"):byte() do + add_char(b) + end + + for b = ("a"):byte(), ("z"):byte() do + add_char(b) + end + + add_extra("extra1") + add_extra("extra2") + + local expected_part1 = table.concat(expected_parts1) + local expected_part2 = table.concat(expected_parts2) + local expected_part3 = table.concat(expected_parts3):sub(1, -2) + local expected = expected_part1 .. expected_part2 .. expected_part3 .. "},{},{}}" + + assert.same(expected, cache.serialize({ - events = { - {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"} - }, - per_line_options = {}, + warnings = warnings, + inline_options = {}, line_lengths = {} }) ) end) it("handles error result", function() - assert.same('return {{{"011",[3]=2,[4]=4,[13]="message"}},{},{}}', cache.serialize({ - events = { + assert.same('return {{{"011",2,4,nil,"message"}},{},{}}', cache.serialize({ + warnings = { {code = "011", line = 2, column = 4, msg = "message"} }, - per_line_options = {}, + inline_options = {}, line_lengths = {} })) end) @@ -120,10 +120,10 @@ local function report(code) return { - events = { + warnings = { code and {code = code} }, - per_line_options = {}, + inline_options = {}, line_lengths = {} } end @@ -214,31 +214,28 @@ local tmpname local foo_report = { - events = { + warnings = { {code = "111", name = "not_print", line = 1, column = 1}, - {push = true, line = 2, column = 1}, - {options = {std = "none"}, line = 3, column = 1}, {code = "111", name = "not_print", line = 4, column = 1}, {code = "111", name = "print", line = 5, column = 1}, - {pop = true, line = 6, column = 1}, {code = "111", name = "print", line = 7, column = 1}, - {options = {std = "bad_std"}, line = 8, column = 1} }, - per_line_options = { - [4] = { - {options = {ignore = {",*"}}, line = 4, column = 10} - }, - [1000] = { - {options = {std = "max"}, line = 1000, column = 1}, - {options = {std = "another_bad_std"}, line = 1000, column = 20} - } + inline_options = { + {options = {std = "none"}, line = 3, column = 1}, + {options = {ignore = {",*"}}, line = 4, column = 10}, + {pop_count = 1, line = 5}, + {pop_count = 1, line = 6}, + {options = {std = "bad_std"}, line = 8, column = 1}, + {options = {std = "max"}, line = 1000, column = 1}, + {pop_count = 1, options = {std = "another_bad_std"}, line = 1001, column = 20}, + {pop_count = 1, line = 1002}, }, line_lengths = {10, 20, 30} } local bar_report = { - events = {{code = "011", line = 2, column = 4, msg = "message"}}, - per_line_options = {}, + warnings = {{code = "011", line = 2, column = 4, msg = "message"}}, + inline_options = {}, line_lengths = {40, 50} } diff -Nru luacheck-0.22.0/spec/check_spec.lua luacheck-0.23.0/spec/check_spec.lua --- luacheck-0.22.0/spec/check_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/check_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,21 +1,21 @@ local raw_check = require "luacheck.check" -local function remove_cyclomatic_complexity_warnings(events) - for i = #events, 1, -1 do - if events[i].code == "561" then - table.remove(events, i) +local function remove_cyclomatic_complexity_warnings(warnings) + for i = #warnings, 1, -1 do + if warnings[i].code == "561" then + table.remove(warnings, i) end end end local function check_full(src) local report = raw_check(src) - remove_cyclomatic_complexity_warnings(report.events) + remove_cyclomatic_complexity_warnings(report.warnings) return report end local function check(src) - return check_full(src).events + return check_full(src).warnings end describe("check", function() @@ -23,38 +23,13 @@ assert.same({}, check("")) end) - it("detects duplicated fields in table literals", function() - assert.same({ - {code = "314", field = "key", line = 3, column = 4, end_column = 4, - overwritten_line = 7, overwritten_column = 4, overwritten_end_column = 6}, - {code = "314", field = "2", index = true, line = 6, column = 4, end_column = 4, - overwritten_line = 9, overwritten_column = 4, overwritten_end_column = 4}, - {code = "314", field = "key", line = 7, column = 4, end_column = 6, - overwritten_line = 8, overwritten_column = 4, overwritten_end_column = 6}, - {code = "314", field = "0.2e1", line = 9, column = 4, end_column = 4, - overwritten_line = 10, overwritten_column = 4, overwritten_end_column = 4} - }, check[[ -local x, y, z = 1, 2, 3 -return { - ["key"] = 4, - [z] = 7, - 1, - y, - key = x, - key = 0, - [0.2e1] = 6, - [2] = 7 -} -]]) - 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, overwritten_line = 2, overwritten_column = 1, overwritten_end_column = 1}, {code = "311", name = "b", line = 1, column = 10, end_column = 10, overwritten_line = 2, overwritten_column = 4, overwritten_end_column = 4}, - {code = "532", line = 2, column = 6, end_column = 6} + {code = "532", line = 2, column = 1, end_column = 12} }, check[[ local a, b = "foo", "bar" a, b = "bar" @@ -82,8 +57,8 @@ it("does not detect unused values in loops", function() assert.same({ - {code = "113", name = "print", indexing = {"print"}, line = 3, column = 4, end_column = 8}, - {code = "113", name = "math", indexing = {"math", "floor"}, line = 4, column = 8, end_column = 11} + {code = "113", name = "print", line = 3, column = 4, end_column = 8}, + {code = "113", name = "math", indexing = {"floor"}, line = 4, column = 8, end_column = 11} }, check[[ local a = 10 while a > 0 do @@ -140,7 +115,7 @@ {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, prev_end_column = 9}, - {code = "113", name = "print", indexing = {"print"}, line = 3, column = 1, end_column = 5} + {code = "113", name = "print", line = 3, column = 1, end_column = 5} }, check[[ local foo local foo = "bar" @@ -241,45 +216,10 @@ ]]) 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 = "541", line = 2, column = 1, end_column = 6}, {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} @@ -302,26 +242,38 @@ ]]) end) - it("emits events, per-line options, and line lengths", function() + it("provides correct locations in presence of utf8", function() assert.same({ - events = { - {push = true, line = 1, column = 1, end_column = 28}, - {options = {ignore = {"bar"}}, line = 1, column = 1, end_column = 28}, + {code = "211", name = "a", line = 2, column = 15, end_column = 15}, + {code = "113", name = "math", line = 2, column = 17, end_column = 20, indexing = {"\204\130"}} + }, check("-- \240\144\128\128\224\166\152\nlocal --[[\204\128]] a;math['\204\130']()\n")) + end) + + it("provides inline options, line lengths, and line endings", function() + assert.same({ + warnings = { {code = "211", name = "foo", line = 2, column = 7, end_column = 9}, {code = "211", name = "bar", line = 2, column = 12, end_column = 14}, - {pop = true, line = 3, column = 1, end_column = 16}, - {push = true, closure = true, line = 4, column = 8}, - {options = {ignore = {".*"}}, line = 5, column = 1, end_column = 19}, - {code = "512", line = 7, column = 1, end_column = 3}, + {code = "512", line = 7, column = 1, end_column = 32}, {code = "213", name = "_", line = 7, column = 5, end_column = 5}, - {code = "113", name = "pairs", indexing = {"pairs"}, line = 7, column = 10, end_column = 14}, - {pop = true, closure = true, line = 9, column = 1} + {code = "113", name = "pairs", line = 7, column = 10, end_column = 14}, + {code = "211", name = "f", func = true, line = 11, column = 16, end_column = 16} }, - per_line_options = { - [2] = {{options = {ignore = {"foo"}}, line = 2, column = 16, end_column = 38}} + inline_options = { + {options = {ignore = {"bar"}}, line = 1, column = 1, end_column = 28}, + {options = {ignore = {"foo"}}, line = 2, column = 16, end_column = 38}, + {pop_count = 1, line = 3}, + {pop_count = 1, line = 4}, + {options = {ignore = {".*"}}, line = 5, column = 1, end_column = 19}, + {options = {ignore = {"f"}}, line = 11, column = 24, end_column = 44}, + {pop_count = 1, options = {std = "max"}, line = 12, column = 1, end_column = 20}, + {options = {std = "none"}, line = 13, column = 1, end_column = 21}, + {pop_count = 2, line = 15}, + {pop_count = 1, line = 16} }, - line_lengths = {28, 38, 16, 17, 19, 17, 32, 16, 3}, - line_endings = {"comment", "comment", "comment", nil, "comment", "comment", nil, "comment", nil} + line_lengths = {28, 38, 16, 17, 19, 17, 32, 16, 0, 17, 44, 20, 21, 16, 3, 0}, + line_endings = {"comment", "comment", "comment", nil, "comment", "comment", nil, "comment", nil, + "comment", "comment", "comment", "comment", "comment"} }, check_full[[ -- luacheck: push ignore bar local foo, bar -- luacheck: ignore foo @@ -331,6 +283,12 @@ -- luacheck: push for _ in pairs({}) do return end -- luacheck: pop + +-- luacheck: push +local function f() end -- luacheck: ignore f +-- luacheck: std max +-- luacheck: std none +-- luacheck: pop end ]]) end) @@ -358,10 +316,10 @@ -- luacheck: no ignore anything please -- luacheck: -- luacheck: no unused, , no redefined -]].events) +]].warnings) end) it("handles argparse sample", function() - assert.table(check(io.open("spec/samples/argparse.lua", "rb"):read("*a"))) + assert.table(check(io.open("spec/samples/argparse-0.2.0.lua", "rb"):read("*a"))) end) end) diff -Nru luacheck-0.22.0/spec/cli_spec.lua luacheck-0.23.0/spec/cli_spec.lua --- luacheck-0.22.0/spec/cli_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/cli_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -68,6 +68,10 @@ assert.equal(0, get_exitcode "spec/samples/good_code.lua --no-config") end) + it("allows measuring performance", function() + assert.equal(0, get_exitcode "spec/samples/good_code.lua --no-config --profile") + end) + it("removes ./ in the beginnings of file names", function() assert.equal([[ Checking spec/samples/good_code.lua OK @@ -415,10 +419,10 @@ end) it("handles bad rockspecs", function() - assert.equal([[ -Checking spec/samples/bad.rockspec Syntax error + assert.matches([[ +Checking spec/samples/bad.rockspec Runtime error - spec/samples/bad.rockspec: rockspec.build is not a table + spec/samples/bad%.rockspec: line 1: attempt to call .+ Total: 0 warnings / 0 errors in 0 files, couldn't check 1 file ]], get_output "spec/samples/bad.rockspec --no-config") @@ -529,8 +533,8 @@ 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: right side of assignment has less values than left side expects - spec/samples/bad_flow.lua:16:15: right side of assignment has more values than left side expects + spec/samples/bad_flow.lua:12:10: right side of assignment has less values than left side expects + spec/samples/bad_flow.lua:16:10: right side of assignment has more values than left side expects spec/samples/bad_flow.lua:21:7: unreachable code spec/samples/bad_flow.lua:25:1: loop is executed at most once @@ -683,6 +687,16 @@ ]], get_output "spec/samples/global_fields.lua --config=spec/configs/custom_fields_config.luacheckrc") end) + it("detects fornums going from #(expr) down to 1 with positive step", function() + assert.equal([[ +Checking spec/samples/reversed_fornum.lua 1 warning + + spec/samples/reversed_fornum.lua:1:1: numeric for loop goes from #(expr) down to -1.5 but loop step is not negative + +Total: 1 warning / 0 errors in 1 file +]], get_output "spec/samples/reversed_fornum.lua --no-config") + end) + it("allows showing warning codes", function() assert.equal([[ Checking spec/samples/read_globals.lua 5 warnings @@ -709,7 +723,7 @@ 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:34:1-6: empty do..end block spec/samples/inline_options.lua:35:10-13: empty if branch Checking spec/samples/python_code.lua 1 error @@ -720,6 +734,23 @@ ]], get_output "spec/samples/inline_options.lua spec/samples/python_code.lua --ranges --no-config") end) + it("shows correct ranges for files with utf8", function() + assert.equal([[ +Checking spec/samples/utf8.lua 4 warnings + + spec/samples/utf8.lua:2:1-4: setting undefined field '분야 명' of global 'math' + spec/samples/utf8.lua:2:16-19: accessing undefined field '値' of global 'math' + spec/samples/utf8.lua:3:25-25: unused variable 't' + spec/samples/utf8.lua:4:5-28: value assigned to field 'päällekkäinen nimi a\u{200B}b' is overwritten on line 5 before use + +Checking spec/samples/utf8_error.lua 1 error + + spec/samples/utf8_error.lua:2:11-11: expected statement near 'о' + +Total: 4 warnings / 1 error in 2 files +]], get_output "spec/samples/utf8.lua spec/samples/utf8_error.lua --ranges --no-config") + end) + it("applies inline options", function() assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors @@ -801,41 +832,6 @@ ]], 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 @@ -913,19 +909,19 @@ end -- luacheck: push no max string line length - local format_version, good_mtime, bad_mtime, python_mtime = cache:match(replace_abspath(([[ + local format_version, good_mtime, bad_mtime, python_mtime = cache:match(replace_abspath([[ (%d+) abspath{spec/samples/good_code.lua} (%d+) -local A,B="561","function";return {{{A,[3]=1,[4]=1,[5]=1,[29]=1,[31]="main_chunk"},{A,[3]=3,[4]=7,[5]=14,[29]=1,[30]="helper",[31]=B},{A,[3]=7,[4]=1,[5]=8,[29]=2,[30]="embracer.embrace",[31]=B}},{},{19,0,23,17,3,0,30,25,26,3,0,15},{[4]="comment"}} +local A,B="561","function";return {{{A,1,1,1,1,"main_chunk"},{A,3,7,23,1,B,"helper"},{A,7,1,30,2,B,"embracer.embrace"}},{},{19,0,23,17,3,0,30,25,26,3,0,15,0},{[4]="comment"}} abspath{spec/samples/bad_code.lua} (%d+) -local A,B,C,D,E,F="package","561","helper","function","embrace","hepler";return {{{"112",A,1,1,7,[24]={A,"loaded",true}},{B,[3]=1,[4]=1,[5]=1,[29]=1,[31]="main_chunk"},{B,[3]=3,[4]=7,[5]=14,[29]=1,[30]=C,[31]=D},{"211",C,3,16,21,[11]=true},{"212","...",3,23,25},{B,[3]=7,[4]=1,[5]=8,[29]=2,[30]=E,[31]=D},{"111",E,7,10,16,[12]=true,[24]={E}},{"412","opt",8,10,12,7,18,20},{"113",F,9,11,16,[24]={F}}},{},{24,0,26,9,3,0,21,31,26,3,0},{[4]="comment"}} +local A,B,C,D="561","helper","function","embrace";return {{{"112",1,1,7,"package",{"loaded",true}},{A,1,1,1,1,"main_chunk"},{A,3,7,26,1,C,B},{"211",3,16,21,B,true},{"212",3,23,25,"..."},{A,7,1,21,2,C,D},{"111",7,10,16,D,nil,nil,true},{"412",8,10,12,"opt",7,18,20},{"113",9,11,16,"hepler"}},{},{24,0,26,9,3,0,21,31,26,3,0,0},{[4]="comment"}} abspath{spec/samples/python_code.lua} (%d+) -return {{{"011",[3]=1,[4]=6,[5]=15,[13]="expected '=' near '__future__'"}},{},{},{}} -]]):gsub("[%[%]]", "%%%0"))) +return {{{"011",1,6,15,"expected '=' near '__future__'"}},{},{},{}} +]]):gsub("[%[%]%-]", "%%%0"), nil) -- luacheck: pop format_version = tonumber(format_version) @@ -943,10 +939,10 @@ %s abspath{spec/samples/python_code.lua} %s -return {{{"111", "global", 1, 1, [24]={"global"}}, {"321", "uninit", 6, 8}},{},{},{}} +return {{{"111", 1, 1, nil, "global"}, {"321", 6, 8, nil, "uninit"}},{},{1, 1, 1, 1, 1, 1},{}} abspath{spec/samples/good_code.lua} %s -return {{{"011",[3]=5,[4]=7,[13]="this code is actually bad"}},{},{},{}} +return {{{"011",5,7,nil, "this code is actually bad"}},{},{},{}} abspath{spec/samples/bad_code.lua} %s return {{},{},{}}]]):format(version, python_mtime, good_mtime, tostring(tonumber(bad_mtime) - 1))) @@ -1122,11 +1118,11 @@ 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$")) + assert.truthy(output:match("^Luacheck: [%w%p ]+\nLua: [%w%p ]+\nArgparse: [%w%p ]+\nLuaFileSystem: [%w%p ]+\nLuaLanes: [%w%p ]+\n$")) end) it("expands folders", function() - assert.matches("^Total: %d+ warnings / %d+ errors in 23 files\n$", get_output "spec/samples -qqq --no-config --exclude-files spec/samples/global_fields.lua") + assert.matches("^Total: %d+ warnings / %d+ errors in 26 files\n$", get_output "spec/samples -qqq --no-config --exclude-files spec/samples/global_fields.lua") end) it("uses --include-files when expanding folders", function() @@ -1214,6 +1210,71 @@ ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc") end) + it("adds per-file overrides with default stds", function() + assert.equal(([[ +Checking .luacheckrc 3 warnings + + .luacheckrc:14:6: accessing undefined variable 'it' + .luacheckrc:14:10: accessing undefined variable 'version' + .luacheckrc:14:25: accessing undefined variable 'newproxy' + +Checking default_stds-scm-1.rockspec 3 warnings + + default_stds-scm-1.rockspec:13:1: accessing undefined variable 'it' + default_stds-scm-1.rockspec:13:21: accessing undefined variable 'newproxy' + default_stds-scm-1.rockspec:13:37: accessing undefined variable 'new_globals' + +Checking nested/spec/sample_spec.lua 3 warnings + + nested/spec/sample_spec.lua:1:39: accessing undefined variable 'newproxy' + nested/spec/sample_spec.lua:1:55: accessing undefined variable 'version' + nested/spec/sample_spec.lua:1:64: accessing undefined variable 'read_globals' + +Checking normal_file.lua 4 warnings + + normal_file.lua:1:1: accessing undefined variable 'it' + normal_file.lua:1:29: accessing undefined variable 'newproxy' + normal_file.lua:1:45: accessing undefined variable 'version' + normal_file.lua:1:54: accessing undefined variable 'read_globals' + +Checking sample_spec.lua 4 warnings + + sample_spec.lua:1:1: accessing undefined variable 'it' + sample_spec.lua:1:28: accessing undefined variable 'newproxy' + sample_spec.lua:1:44: accessing undefined variable 'version' + sample_spec.lua:1:53: accessing undefined variable 'read_globals' + +Checking test/nested_normal_file.lua 4 warnings + + test/nested_normal_file.lua:1:1: accessing undefined variable 'it' + test/nested_normal_file.lua:1:47: accessing undefined variable 'newproxy' + test/nested_normal_file.lua:1:63: accessing undefined variable 'version' + test/nested_normal_file.lua:1:72: accessing undefined variable 'read_globals' + +Checking test/sample_spec.lua 5 warnings + + test/sample_spec.lua:1:1: accessing undefined variable 'it' + test/sample_spec.lua:1:37: accessing undefined variable 'newproxy' + test/sample_spec.lua:1:47: accessing undefined variable 'math' + test/sample_spec.lua:1:53: accessing undefined variable 'version' + test/sample_spec.lua:1:62: accessing undefined variable 'read_globals' + +Checking tests/nested/sample_spec.lua 3 warnings + + tests/nested/sample_spec.lua:1:44: accessing undefined variable 'newproxy' + tests/nested/sample_spec.lua:1:60: accessing undefined variable 'version' + tests/nested/sample_spec.lua:1:69: accessing undefined variable 'read_globals' + +Checking tests/sample_spec.lua 3 warnings + + tests/sample_spec.lua:1:17: accessing undefined variable 'newproxy' + tests/sample_spec.lua:1:33: accessing undefined variable 'version' + tests/sample_spec.lua:1:42: accessing undefined variable 'read_globals' + +Total: 32 warnings / 0 errors in 9 files +]]):gsub("([a-z])/", "%1" .. package.config:sub(1, 1)), get_output(". --include-files .", "spec/projects/default_stds/")) + end) + it("uses new filename when selecting per-file overrides", function() assert.equal([[ Checking spec/samples/unused_code.lua OK @@ -1271,7 +1332,7 @@ it("uses exclude_files option", function() assert.equal(([[ -Checking spec/samples/argparse.lua 9 warnings +Checking spec/samples/argparse-0.2.0.lua 9 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 @@ -1283,17 +1344,20 @@ 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/reversed_fornum.lua 1 warning Checking spec/samples/unused_code.lua 9 warnings Checking spec/samples/unused_secondaries.lua 4 warnings +Checking spec/samples/utf8.lua 4 warnings +Checking spec/samples/utf8_error.lua 1 error -Total: 67 warnings / 4 errors in 16 files +Total: 72 warnings / 5 errors in 19 files ]]):gsub("(spec/samples)/", "%1"..package.config:sub(1, 1)), get_output "spec/samples --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files spec/samples/global_fields.lua") end) it("loads exclude_files option correctly from upper directory", function() assert.equal([[ -Checking argparse.lua 9 warnings +Checking argparse-0.2.0.lua 9 warnings Checking compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings @@ -1305,16 +1369,19 @@ Checking read_globals.lua 5 warnings Checking read_globals_inline_options.lua 3 warnings Checking redefined.lua 7 warnings +Checking reversed_fornum.lua 1 warning Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings +Checking utf8.lua 4 warnings +Checking utf8_error.lua 1 error -Total: 67 warnings / 4 errors in 16 files +Total: 72 warnings / 5 errors in 19 files ]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua", "spec/samples/")) end) it("combines excluded files from config and cli", function() assert.equal([[ -Checking argparse.lua 9 warnings +Checking argparse-0.2.0.lua 9 warnings Checking compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings @@ -1324,10 +1391,13 @@ Checking line_length.lua 8 warnings Checking python_code.lua 1 error Checking redefined.lua 7 warnings +Checking reversed_fornum.lua 1 warning Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings +Checking utf8.lua 4 warnings +Checking utf8_error.lua 1 error -Total: 59 warnings / 4 errors in 14 files +Total: 64 warnings / 5 errors in 17 files ]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua --exclude-files " .. quote("./read*"), "spec/samples/")) end) @@ -1426,12 +1496,12 @@ it("does not use global path as fallback if --config is used", function() assert.equal([[ Files: 1 -Warnings: 2 +Warnings: 4 Errors: 0 Quiet: 0 Color: false Codes: true -]], get_output "spec/samples/compat.lua --default-config=spec/configs/global_config.luacheckrc --config=spec/configs/cli_specific_config.luacheckrc") +]], get_output "spec/samples/compat.lua --std=min --default-config=spec/configs/global_config.luacheckrc --config=spec/configs/cli_specific_config.luacheckrc") end) it("does not use global path as fallback if --no-config is used", function() diff -Nru luacheck-0.22.0/spec/cyclomatic_complexity_spec.lua luacheck-0.23.0/spec/cyclomatic_complexity_spec.lua --- luacheck-0.22.0/spec/cyclomatic_complexity_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/cyclomatic_complexity_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,22 +1,7 @@ -local core_utils = require "luacheck.core_utils" -local detect_cyclomatic_complexity = require "luacheck.detect_cyclomatic_complexity" -local linearize = require "luacheck.linearize" -local name_functions = require "luacheck.name_functions" -local parser = require "luacheck.parser" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - name_functions(chstate) - chstate.warnings = {} - detect_cyclomatic_complexity(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end +local helper = require "spec.helper" local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) + assert.same(warnings, helper.get_stage_warnings("detect_cyclomatic_complexity", src)) end describe("cyclomatic complexity detection", function() @@ -133,20 +118,20 @@ it("provides appropriate names and types for functions", function() assert_warnings({ {code = "561", line = 1, column = 1, end_column = 1, complexity = 1, function_type = "main_chunk"}, - {code = "561", line = 1, column = 8, end_column = 15, complexity = 1,function_type = "function"}, - {code = "561", line = 2, column = 14, end_column = 21, complexity = 1, function_type = "function", + {code = "561", line = 1, column = 8, end_column = 17, complexity = 1,function_type = "function"}, + {code = "561", line = 2, column = 14, end_column = 27, complexity = 1, function_type = "function", function_name = "f"}, - {code = "561", line = 3, column = 8, end_column = 15, complexity = 1, function_type = "function", + {code = "561", line = 3, column = 8, end_column = 21, complexity = 1, function_type = "function", function_name = "g"}, - {code = "561", line = 4, column = 10, end_column = 17, complexity = 1, function_type = "function", + {code = "561", line = 4, column = 10, end_column = 25, complexity = 1, function_type = "function", function_name = "h"}, - {code = "561", line = 5, column = 25, end_column = 32, complexity = 1, function_type = "function", + {code = "561", line = 5, column = 25, end_column = 38, complexity = 1, function_type = "function", function_name = "t.k"}, - {code = "561", line = 6, column = 26, end_column = 33, complexity = 1, function_type = "function", + {code = "561", line = 6, column = 26, end_column = 39, complexity = 1, function_type = "function", function_name = "t.k1.k2.k3.k4"}, - {code = "561", line = 7, column = 11, end_column = 18, complexity = 1, function_type = "function"}, - {code = "561", line = 8, column = 6, end_column = 13, complexity = 1, function_type = "function"}, - {code = "561", line = 9, column = 4, end_column = 11, complexity = 1, function_type = "method", + {code = "561", line = 7, column = 11, end_column = 24, complexity = 1, function_type = "function"}, + {code = "561", line = 8, column = 6, end_column = 19, complexity = 1, function_type = "function"}, + {code = "561", line = 9, column = 4, end_column = 27, complexity = 1, function_type = "method", function_name = "t.foo.bar"} }, [[ return function() diff -Nru luacheck-0.22.0/spec/decoder_spec.lua luacheck-0.23.0/spec/decoder_spec.lua --- luacheck-0.22.0/spec/decoder_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/decoder_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,92 @@ +local decoder = require "luacheck.decoder" +local lua_utf8 = require "lua-utf8" + +local function assert_encoding(encoding, ...) + local lib = encoding == "utf8" and lua_utf8 or string + local length = select("#", ...) + local bytes = lib.char(...) + local chars = decoder.decode(bytes) + + local label_parts = {"("} + + for index = 1, length do + table.insert(label_parts, ("\\u{%X}"):format((select(index, ...)))) + end + + table.insert(label_parts, ")") + local label = table.concat(label_parts) + + assert.equals(length, chars:get_length(), ":get_length" .. label) + + for from = 1, length do + for to = from, length do + assert.equals(lib.sub(bytes, from, to), chars:get_substring(from, to), ":get_substring" .. label) + end + end + + local iter, state, var + + if encoding == "utf8" then + iter, state = lua_utf8.next, bytes + else + iter, state, var = ipairs({...}) + end + + local index = 1 + + for offset, codepoint in iter, state, var do + assert.equals(codepoint, chars:get_codepoint(index), ":get_codepoint" .. label) + + local from, to, match = chars:find("(.)", index) + assert.equals(offset, from, ":find" .. label) + assert.equals(offset, to, ":find" .. label) + assert.equals(bytes:sub(offset, offset), match, ":find" .. label) + index = index + 1 + end +end + +describe("decoder", function() + it("decodes valid codepoints correctly", function() + -- Checking literally all codepoints is very slow with coverage enabled, pick only a few. + for base = 0, 0x10FFFF, 0x800 do + for offset = 0, 0x100, 41 do + local codepoint1 = base + offset + local codepoint2 = codepoint1 + 9 + assert_encoding("utf8", codepoint1, codepoint2) + end + end + end) + + it("falls back to latin1 on invalid utf8", function() + -- Bad first byte. + assert_encoding("latin1", 0xC0, 0x80, 0x80, 0x80) + assert_encoding("latin1", 0x00, 0xF8, 0x80, 0x80, 0x80) + + -- Two bytes, bad continuation byte. + assert_encoding("latin1", 0x00, 0xC0, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xC0, 0xFF, 0xC0, 0x80) + + -- Three bytes, bad first continuation byte. + assert_encoding("latin1", 0x00, 0xE0, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xE0, 0xFF, 0xC0, 0x80) + + -- Three bytes, bad second continuation byte. + assert_encoding("latin1", 0x00, 0xE0, 0x80, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xE0, 0x80, 0xFF, 0xC0, 0x80) + + -- Four bytes, bad first continuation byte. + assert_encoding("latin1", 0x00, 0xF0, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xF0, 0xFF, 0xC0, 0x80) + + -- Four bytes, bad second continuation byte. + assert_encoding("latin1", 0x00, 0xF0, 0x80, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xF0, 0x80, 0xFF, 0xC0, 0x80) + + -- Four bytes, bad third continuation byte. + assert_encoding("latin1", 0x00, 0xF0, 0x80, 0x80, 0x00, 0xC0, 0x80) + assert_encoding("latin1", 0x00, 0xF0, 0x80, 0x80, 0xFF, 0xC0, 0x80) + + -- Codepoint too large. + assert_encoding("latin1", 0xF7, 0x80, 0x80, 0x80, 0x00) + end) +end) diff -Nru luacheck-0.22.0/spec/empty_blocks_spec.lua luacheck-0.23.0/spec/empty_blocks_spec.lua --- luacheck-0.22.0/spec/empty_blocks_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/empty_blocks_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,68 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_empty_blocks", src)) +end + +describe("empty block detection", function() + it("detects empty blocks", function() + assert_warnings({ + {code = "541", line = 1, column = 1, end_column = 6}, + {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} + }, [[ +do end + +if ... then + +elseif ... then + +else + +end + +if ... then + somehing() +else + something_else() +end + +do something() end + +while ... do end +repeat until ... +]]) + end) + + it("detects empty blocks in nested blocks and functions", function() + assert_warnings({ + {code = "541", line = 4, column = 10, end_column = 15}, + {code = "541", line = 7, column = 13, end_column = 18}, + {code = "541", line = 12, column = 22, end_column = 27}, + {code = "542", line = 14, column = 27, end_column = 30} + }, [[ +do + while x do + if y then + do end + else + repeat + do end + + function t() + for i = 1, 10 do + for _, v in ipairs(tab) do + do end + + if c then end + end + end + end + until z + end + end +end +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/expand_rockspec_spec.lua luacheck-0.23.0/spec/expand_rockspec_spec.lua --- luacheck-0.22.0/spec/expand_rockspec_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/expand_rockspec_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,4 +1,6 @@ local expand_rockspec = require "luacheck.expand_rockspec" +local fs = require "luacheck.fs" +local P = fs.normalize describe("expand_rockspec", function() it("returns sorted array of lua files related to a rock", function() @@ -10,6 +12,20 @@ }, expand_rockspec("spec/folder/rockspec")) end) + it("autodetects modules for rockspecs without build table", function() + assert.same({ + P"spec/rock/src/rock.lua", + P"spec/rock/src/rock/mod.lua", + P"spec/rock/bin/rock.lua" + }, expand_rockspec("spec/rock/rock-dev-1.rockspec")) + end) + + it("autodetects modules for rockspecs without build.modules table", function() + assert.same({ + P"spec/rock2/mod.lua" + }, expand_rockspec("spec/rock2/rock2-dev-1.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) diff -Nru luacheck-0.22.0/spec/filter_spec.lua luacheck-0.23.0/spec/filter_spec.lua --- luacheck-0.22.0/spec/filter_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/filter_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -4,11 +4,15 @@ local report = {} for i, issues in ipairs(issue_arrays) do + local line_lengths = {} + for issue_index, issue in ipairs(issues) do issue.line = issue_index + issue.column = 1 + line_lengths[issue_index] = 0 end - report[i] = {events = issues, per_line_options = {}, line_lengths = {}} + report[i] = {warnings = issues, inline_options = {}, line_lengths = line_lengths, line_endings = {}} end local result = filter_full(report, opts) @@ -16,6 +20,7 @@ for _, file_report in ipairs(result) do for _, issue in ipairs(file_report) do issue.line = nil + issue.column = nil end end @@ -104,8 +109,7 @@ }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "413", @@ -125,8 +129,7 @@ }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "413", @@ -141,8 +144,7 @@ }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "321", @@ -292,21 +294,18 @@ { { code = "111", - name = "module", - indexing = {"module"} + name = "module" } } }, filter({ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" }, { code = "111", - name = "module", - indexing = {"module"} + name = "module" } } }, { @@ -320,21 +319,18 @@ { { code = "111", - name = "module", - indexing = {"module"} + name = "module" } } }, filter({ { { code = "113", - name = "package", - indexing = {"package"} + name = "package" }, { code = "111", - name = "module", - indexing = {"module"} + name = "module" } } }, { @@ -347,13 +343,11 @@ { { code = "131", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "113", - name = "baz", - indexing = {"baz"} + name = "baz" } } }, filter({ @@ -368,13 +362,11 @@ }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "113", - name = "baz", - indexing = {"baz"} + name = "baz" } } }, { @@ -387,37 +379,31 @@ { { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "113", - name = "baz", - indexing = {"baz"} + name = "baz" } } }, filter({ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" }, { code = "111", name = "foo", - indexing = {"foo"}, top = true }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" }, { code = "113", - name = "baz", - indexing = {"baz"} + name = "baz" } } }, { @@ -431,28 +417,24 @@ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } } }, filter({ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" }, { code = "111", - name = "foo", - indexing = {"foo"} + name = "foo" } }, { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } } }, { @@ -468,13 +450,11 @@ { code = "111", name = "string", - indexing = {"string"}, module = true }, { code = "111", name = "bar", - indexing = {"bar"}, module = true } } @@ -482,31 +462,26 @@ { { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" } }, { { code = "111", name = "foo", - indexing = {"foo"}, top = true }, { code = "111", - name = "foo", - indexing = {"foo"} + name = "foo" }, { code = "111", - name = "string", - indexing = {"string"} + name = "string" }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" } } }, { @@ -529,20 +504,17 @@ { { code = "111", - name = "foo", - indexing = {"foo"} + name = "foo" } }, { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" }, { code = "111", - name = "bar", - indexing = {"bar"} + name = "bar" } } }, { @@ -559,78 +531,43 @@ it("applies inline option events and per-line options", function() assert.same({ { - {code = "111", name = "not_print", indexing = {"not_print"}, line = 1, column = 1}, - {code = "111", name = "print", indexing = {"print"}, line = 5, column = 1}, - {code = "121", name = "print", indexing = {"print"}, line = 7, column = 1}, + {code = "111", name = "not_print", line = 1, column = 1}, + {code = "111", name = "print", line = 5, column = 1}, + {code = "121", name = "print", line = 7, column = 1}, {code = "021", msg = "invalid value of option 'std': unknown std 'bad_std'", line = 8, column = 1}, {code = "021", msg = "invalid value of option 'std': unknown std 'another_bad_std'", - line = 1000,column = 20} + line = 11, column = 20}, + {code = "211", name = "not_print", line = 14, column = 1} } }, filter_full({ { - events = { - {code = "111", name = "not_print", indexing = {"not_print"}, line = 1, column = 1}, - {push = true, line = 2, column = 1}, - {options = {std = "none"}, line = 3, column = 1}, - {code = "111", name = "not_print", indexing = {"not_print"}, line = 4, column = 1}, - {code = "111", name = "print", indexing = {"print"}, line = 5, column = 1}, - {pop = true, line = 6, column = 1}, - {code = "111", name = "print", indexing = {"print"}, line = 7, column = 1}, - {options = {std = "bad_std"}, line = 8, column = 1} - }, - per_line_options = { - [4] = { - {options = {ignore = {",*"}}, line = 4, column = 10} - }, - [1000] = { - {options = {std = "max"}, line = 1000, column = 1}, - {options = {std = "another_bad_std"}, line = 1000, column = 20} - } + warnings = { + {code = "111", name = "not_print", line = 1, column = 1}, + {code = "111", name = "not_print", line = 4, column = 1}, + {code = "111", name = "print", line = 5, column = 1}, + {code = "111", name = "print", line = 7, column = 1}, + {code = "111", name = "not_print", line = 12, column = 1}, + {code = "211", name = "not_print", line = 14, column = 1}, + {code = "311", name = "c", line = 14, column = 2} }, - line_lengths = {} - } - }, { - { - std = "max" - } - })) - end) - - it("ignores inline options completely with inline = false", function() - assert.same({ - { - {code = "111", name = "not_print", indexing = {"not_print"}, line = 1, column = 1}, - {code = "111", name = "not_print", indexing = {"not_print"}, line = 4, column = 1}, - {code = "121", name = "print", indexing = {"print"}, line = 5, column = 1}, - {code = "121", name = "print", indexing = {"print"}, line = 7, column = 1} - } - }, filter_full({ - { - events = { - {code = "111", name = "not_print", indexing = {"not_print"}, line = 1, column = 1}, - {push = true, line = 2, column = 1}, + inline_options = { {options = {std = "none"}, line = 3, column = 1}, - {code = "111", name = "not_print", indexing = {"not_print"}, line = 4, column = 1}, - {code = "111", name = "print", indexing = {"print"}, line = 5, column = 1}, - {pop = true, line = 6, column = 1}, - {code = "111", name = "print", indexing = {"print"}, line = 7, column = 1}, - {options = {std = "bad_std"}, line = 8, column = 1} - }, - per_line_options = { - [4] = { - {options = {ignore = {",*"}}, line = 4, column = 10} - }, - [1000] = { - {options = {std = "max"}, line = 1000, column = 1}, - {options = {std = "another_bad_std"}, line = 1000, column = 20} - } + {options = {ignore = {".*"}}, line = 4, column = 10}, + {pop_count = 1, line = 5}, + {pop_count = 1, line = 7}, + {options = {std = "bad_std"}, line = 8, column = 1}, + {options = {std = "max"}, line = 9, column = 1}, + {options = {std = "another_bad_std"}, line = 11, column = 20}, + {options = {ignore = {"not_print"}}, line = 12, column = 1}, + {options = {ignore = {"211"}}, line = 13, column = 1}, + {pop_count = 2, options = {ignore = {"c"}}, line = 14, column = 1} }, - line_lengths = {} + line_lengths = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + line_endings = {} } }, { { - std = "max", - inline = false + std = "max" } })) end) @@ -643,12 +580,12 @@ } }, filter_full({ { - events = { + warnings = {}, + inline_options = { {options = {max_line_length = 20}, line = 3, column = 1}, {options = {max_string_line_length = 15}, line = 4, column = 1}, {options = {max_line_length = false}, line = 6, column = 1} }, - per_line_options = {}, line_lengths = {120, 121, 15, 20, 18, 15, 200}, line_endings = {[5] = "string"} } diff -Nru luacheck-0.22.0/spec/fs_spec.lua luacheck-0.23.0/spec/fs_spec.lua --- luacheck-0.22.0/spec/fs_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/fs_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,5 +1,3 @@ -local lfs = require "lfs" - local fs = require "luacheck.fs" local utils = require "luacheck.utils" local P = fs.normalize @@ -80,47 +78,3 @@ end) end) end) - -for _, fs_name in ipairs({"lua_fs", "lfs_fs"}) do - local base_fs = require("luacheck." .. fs_name) - - describe(fs_name, function() - describe("get_current_dir", function() - it("returns absolute path to current directory", function() - local current_dir = base_fs.get_current_dir() - assert.string(current_dir) - assert.not_equal("", (fs.split_base(current_dir))) - assert.is_true(fs.is_file(fs.join(current_dir, "spec/folder/foo"))) - end) - end) - - describe("get_mode", function() - local tricky_path = "spec" .. utils.dir_sep .. "'" - - it("returns 'file' for a file", function() - local fh = assert(io.open(tricky_path, "w")) - fh:close() - finally(function() assert(os.remove(tricky_path)) end) - assert.equal("file", base_fs.get_mode(tricky_path)) - end) - - it("returns 'directory' for a directory", function() - assert(lfs.mkdir(tricky_path)) - finally(function() assert(lfs.rmdir(tricky_path)) end) - assert.equal("directory", base_fs.get_mode(tricky_path)) - end) - - it("returns not 'file' or 'directory' if path doesn't point to a file or a directory", function() - local mode = base_fs.get_mode(tricky_path) - assert.not_equal("file", mode) - assert.not_equal("directory", mode) - end) - - it("returns not 'file' or 'directory' if path is bad", function() - local mode = base_fs.get_mode('"^<>!|&%') - assert.not_equal("file", mode) - assert.not_equal("directory", mode) - end) - end) - end) -end diff -Nru luacheck-0.22.0/spec/globals_spec.lua luacheck-0.23.0/spec/globals_spec.lua --- luacheck-0.22.0/spec/globals_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/globals_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,28 +1,13 @@ -local core_utils = require "luacheck.core_utils" -local detect_globals = require "luacheck.detect_globals" -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - resolve_locals(chstate) - chstate.warnings = {} - detect_globals(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end +local helper = require "spec.helper" local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) + assert.same(warnings, helper.get_stage_warnings("detect_globals", src)) end describe("global detection", function() it("detects global set", function() assert_warnings({ - {code = "111", name = "foo", indexing = {"foo"}, line = 1, column = 1, end_column = 3, top = true} + {code = "111", name = "foo", line = 1, column = 1, end_column = 3, top = true} }, [[ foo = {} ]]) @@ -30,7 +15,7 @@ it("detects global set in nested functions", function() assert_warnings({ - {code = "111", name = "foo", indexing = {"foo"}, line = 2, column = 4, end_column = 6} + {code = "111", name = "foo", line = 2, column = 4, end_column = 6} }, [[ local function bar() foo = {} @@ -41,8 +26,8 @@ it("detects global access in multi-assignments", function() assert_warnings({ - {code = "111", name = "y", indexing = {"y"}, line = 2, column = 4, end_column = 4, top = true}, - {code = "113", name = "print", indexing = {"print"}, line = 3, column = 1, end_column = 5} + {code = "111", name = "y", line = 2, column = 4, end_column = 4, top = true}, + {code = "113", name = "print", line = 3, column = 1, end_column = 5} }, [[ local x x, y = 1 @@ -52,8 +37,8 @@ it("detects global access in self swap", function() assert_warnings({ - {code = "113", name = "a", indexing = {"a"}, line = 1, column = 11, end_column = 11}, - {code = "113", name = "print", indexing = {"print"}, line = 2, column = 1, end_column = 5} + {code = "113", name = "a", line = 1, column = 11, end_column = 11}, + {code = "113", name = "print", line = 2, column = 1, end_column = 5} }, [[ local a = a print(a) @@ -62,7 +47,7 @@ it("detects global mutation", function() assert_warnings({ - {code = "112", name = "a", indexing = {"a", false}, line = 1, column = 1, end_column = 1} + {code = "112", name = "a", indexing = {false}, line = 1, column = 1, end_column = 1} }, [[ a[1] = 6 ]]) @@ -73,14 +58,14 @@ { code = "113", name = "b", - indexing = {"b", false}, + indexing = {false}, line = 2, column = 15, end_column = 15 }, { code = "113", name = "b", - indexing = {"b", false, false, "foo"}, + indexing = {false, false, "foo"}, previous_indexing_len = 2, line = 3, column = 8, @@ -99,14 +84,14 @@ { code = "113", name = "b", - indexing = {"b", false}, + indexing = {false}, line = 2, column = 15, end_column = 15 }, { code = "112", name = "b", - indexing = {"b", false, false, "foo"}, + indexing = {false, false, "foo"}, previous_indexing_len = 2, line = 3, column = 1, @@ -120,19 +105,18 @@ ]]) end) - it("provides indexing information for warnings related to globals", function() + it("provides indexing information for warnings related to global fields", function() assert_warnings({ { code = "113", name = "global", - indexing = {"global"}, line = 2, column = 11, end_column = 16 }, { code = "113", name = "global", - indexing = {"global", "foo", "bar", false}, + indexing = {"foo", "bar", false}, indirect = true, previous_indexing_len = 1, line = 3, @@ -141,7 +125,7 @@ }, { code = "113", name = "global", - indexing = {"global", "foo", "bar", false, true}, + indexing = {"foo", "bar", false, true}, indirect = true, previous_indexing_len = 4, line = 5, diff -Nru luacheck-0.22.0/spec/helper.lua luacheck-0.23.0/spec/helper.lua --- luacheck-0.22.0/spec/helper.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/helper.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,5 +1,17 @@ local helper = {} +local function get_lua() + local index = -1 + local res = "lua" + + while arg[index] do + res = arg[index] + index = index - 1 + end + + return res +end + local dir_sep = package.config:sub(1, 1) -- Return path to root directory when run from `path`. @@ -13,22 +25,24 @@ statsfile = prefix.."luacov.stats.out", modules = { luacheck = "src/luacheck/init.lua", - ["luacheck.*"] = "src" + ["luacheck.*"] = "src", + ["luacheck.*.*"] = "src" }, exclude = { - "bin/luacheck$", - "luacheck/argparse$" + "bin/luacheck$" } } end local luacov = package.loaded["luacov.runner"] +local lua -- Returns command that runs `luacheck` executable from `loc_path`. function helper.luacheck_command(loc_path) + lua = lua or get_lua() loc_path = loc_path or "." local prefix = antipath(loc_path) - local cmd = ("cd %s && %s"):format(loc_path, arg[-5] or "lua") + local cmd = ("cd %s && %s"):format(loc_path, lua) -- 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( @@ -42,4 +56,33 @@ return ("%s %sbin%sluacheck.lua"):format(cmd, prefix, dir_sep) end +function helper.get_chstate_after_stage(target_stage_name, source) + -- Luacov isn't yet started when helper is required, defer requiring luacheck + -- modules so that their main chunks get covered. + local check_state = require "luacheck.check_state" + local stages = require "luacheck.stages" + + local chstate = check_state.new(source) + + for index, stage_name in ipairs(stages.names) do + stages.modules[index].run(chstate) + + if stage_name == target_stage_name then + return chstate + end + + chstate.warnings = {} + end + + error("no stage " .. target_stage_name, 0) +end + +function helper.get_stage_warnings(target_stage_name, source) + local core_utils = require "luacheck.core_utils" + + local chstate = helper.get_chstate_after_stage(target_stage_name, source) + core_utils.sort_by_location(chstate.warnings) + return chstate.warnings +end + return helper diff -Nru luacheck-0.22.0/spec/lexer_spec.lua luacheck-0.23.0/spec/lexer_spec.lua --- luacheck-0.22.0/spec/lexer_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/lexer_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,12 +1,17 @@ +local decoder = require "luacheck.decoder" local lexer = require "luacheck.lexer" +local function new_state_from_source_bytes(bytes) + return lexer.new_state(decoder.decode(bytes)) +end + local function get_tokens(source) - local lexer_state = lexer.new_state(source) + local lexer_state = new_state_from_source_bytes(source) local tokens = {} repeat local token = {} - token.token, token.token_value, token.line, token.column, token.offset = lexer.next_token(lexer_state) + token.token, token.token_value, token.line, token.offset = lexer.next_token(lexer_state) tokens[#tokens+1] = token until token.token == "eof" @@ -14,23 +19,23 @@ end local function get_token(source) - local lexer_state = lexer.new_state(source) + local lexer_state = new_state_from_source_bytes(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} + local ok, msg, line, offset, end_offset = lexer.next_token(lexer_state) + return not ok and {msg = msg, line = line, offset = offset, end_offset = end_offset} end local function get_error(source) - return maybe_error(lexer.new_state(source)) + return maybe_error(new_state_from_source_bytes(source)) end local function get_last_error(source) - local lexer_state = lexer.new_state(source) + local lexer_state = new_state_from_source_bytes(source) local err repeat @@ -41,16 +46,6 @@ 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) @@ -137,10 +132,10 @@ 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'"}, + {line = 1, offset = 2, end_offset = 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([["\]])) + assert.same({line = 1, offset = 2, end_offset = 2, msg = "invalid escape sequence '\\'"}, get_error([["\]])) end) it("parses hexadecimal escape sequences correctly", function() @@ -148,23 +143,23 @@ 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'"}, + {line = 1, offset = 2, end_offset = 3, msg = "invalid escape sequence '\\X'"}, get_error([["\XFF"]]) ) assert.same( - {line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\x\"'"}, + {line = 1, offset = 2, end_offset = 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\"'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 4, msg = "invalid hexadecimal escape sequence '\\xx'"}, get_error([["\xxx"]]) ) end) @@ -181,43 +176,43 @@ 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'"}, + {line = 1, offset = 2, end_offset = 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\"'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 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'"}, + {line = 1, offset = 2, end_offset = 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{'"}, + {line = 1, offset = 2, end_offset = 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"]])) + assert.same({line = 1, offset = 2, end_offset = 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([[" + assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished string"}, get_error([["]])) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished string"}, get_error([["']])) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished string"}, get_error([[" "]])) end) end) @@ -244,13 +239,13 @@ 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("[=|")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "invalid long string delimiter"}, get_error("[=")) + assert.same({line = 1, offset = 1, end_offset = 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("[[]")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished long string"}, get_error("[=[\n")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished long string"}, get_error("[[]")) end) end) @@ -264,7 +259,7 @@ 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")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x")) end) it("parses decimal floats correctly", function() @@ -277,32 +272,32 @@ 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.")) + assert.same({line = 1, offset = 1, end_offset = 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")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("1.8e")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("1.8e-")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("1.8E+")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("1.8ee")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("1.8e-e")) + assert.same({line = 1, offset = 1, end_offset = 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")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8p")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8p-")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8P+")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8pF")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8p-F")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x1.8p+LL")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "malformed number"}, get_error("0x.p1")) end) it("parses 64 bits cdata literals correctly", function() @@ -331,51 +326,53 @@ 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")) + assert.same({token = "short_comment", token_value = ""}, get_token("--")) + assert.same({token = "short_comment", token_value = "foo"}, get_token("--foo\nbar")) + assert.same({token = "short_comment", token_value = "["}, get_token("--[")) + assert.same({token = "short_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("--[=[]]")) + assert.same({token = "long_comment", token_value = ""}, get_token("--[[]]")) + assert.same({token = "long_comment", token_value = ""}, get_token("--[[\n]]")) + assert.same({token = "long_comment", token_value = "foo\nbar"}, get_token("--[[foo\nbar]]")) + assert.same({line = 1, offset = 1, end_offset = 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} + {token = "local", line = 1, offset = 1}, + {token = "function", line = 1, offset = 7}, + {token = "name", token_value = "foo", line = 1, offset = 16}, + {token = "(", line = 1, offset = 19}, + {token = "name", token_value = "bar", line = 1, offset = 20}, + {token = ")", line = 1, offset = 23}, + {token = "return", line = 2, offset = 28}, + {token = "name", token_value = "bar", line = 2, offset = 35}, + {token = ":", line = 2, offset = 38}, + {token = "name", token_value = "get_foo", line = 2, offset = 39}, + {token = "string", token_value = "long string\n", line = 2, offset = 46}, + {token = "end", line = 5, offset = 66}, + {token = "short_comment", token_value = " hello", line = 6, offset = 70}, + {token = "name", token_value = "print", line = 7, offset = 79}, + {token = "string", token_value = "123\n", line = 7, offset = 85}, + {token = "short_comment", token_value = " this comment ends just before EOF", line = 10, offset = 113}, + {token = "eof", line = 10, offset = 149} }, get_tokens([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end - +-- hello print "1\z 2\z 3\n" -]])) +-- this comment ends just before EOF]])) 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([[ + assert.same({line = 7, offset = 79, end_offset = 80, msg = "invalid escape sequence '\\g'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string @@ -387,7 +384,7 @@ 3\n" ]])) - assert.same({line = 8, column = 9, end_column = 12, msg = "invalid decimal escape sequence '\\300'"}, + assert.same({line = 8, offset = 89, end_offset = 92, msg = "invalid decimal escape sequence '\\300'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ @@ -400,7 +397,7 @@ 3\n" ]])) - assert.same({line = 8, column = 1, end_column = 1, msg = "malformed number"}, get_last_error([[ + assert.same({line = 8, offset = 79, end_offset = 79, msg = "malformed number"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string @@ -411,7 +408,7 @@ 0xx) ]])) - assert.same({line = 7, column = 7, end_column = 7, msg = "unfinished string"}, get_last_error([[ + assert.same({line = 7, offset = 77, end_offset = 77, msg = "unfinished string"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string @@ -426,28 +423,28 @@ 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} + {token = "name", token_value = "a", line = 1, offset = 1}, + {token = ",", line = 1, offset = 2}, + {token = "name", token_value = "b", line = 1, offset = 3}, + {token = "=", line = 1, offset = 4}, + {token = "number", token_value = "4ll", line = 1, offset = 5}, + {token = "name", token_value = "f", line = 1, offset = 8}, + {token = "=", line = 1, offset = 9}, + {token = "string", token_value = "", line = 1, offset = 10}, + {token = "function", line = 1, offset = 12}, + {token = "name", token_value = "_", line = 1, offset = 21}, + {token = "(", line = 1, offset = 22}, + {token = ")", line = 1, offset = 23}, + {token = "return", line = 1, offset = 24}, + {token = "number", token_value = "1", line = 1, offset = 31}, + {token = "or", line = 1, offset = 32}, + {token = "string", token_value = "", line = 1, offset = 34}, + {token = "end", line = 1, offset = 36}, + {token = "eof", line = 1, 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")) + get_tokens(io.open("spec/samples/argparse-0.2.0.lua", "rb"):read("*a")) end) end) diff -Nru luacheck-0.22.0/spec/linearize_spec.lua luacheck-0.23.0/spec/linearize_spec.lua --- luacheck-0.22.0/spec/linearize_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/linearize_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,15 +1,11 @@ -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" +local helper = require "spec.helper" -local function get_line_(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - return chstate.main_line +local function get_line_or_throw(src) + return helper.get_chstate_after_stage("linearize", src).top_line end local function get_line(src) - local ok, res = pcall(get_line_, src) + local ok, res = pcall(get_line_or_throw, src) if ok or type(res) == "table" then return res @@ -22,7 +18,7 @@ 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 + return "Eval " .. item.node.tag elseif item.tag == "Local" then local buf = {} @@ -53,7 +49,7 @@ 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.empty and ", empty" or "", value.secondaries and (", " .. tostring(#value.secondaries) .. " secondaries") or "", value.secondaries and value.secondaries.used and ", used" or "")) end @@ -79,27 +75,27 @@ 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'"}, + assert.same({line = 1, offset = 1, end_offset = 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"}, + assert.same({line = 1, offset = 1, end_offset = 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"}, + assert.same({line = 1, offset = 28, end_offset = 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, prev_line = 1, prev_column = 1, prev_end_column = 8, + assert.same({line = 2, offset = 10, end_offset = 17, prev_line = 1, prev_offset = 1, prev_end_offset = 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"}, + assert.same({line = 1, offset = 21, end_offset = 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"}, + assert.same({line = 1, offset = 42, end_offset = 44, msg = "cannot use '...' outside a vararg function"}, get_line("function f(...) return function() return ... end end")) end) end) @@ -353,13 +349,13 @@ describe("when registering values", function() it("registers values in empty chunk correctly", function() assert.equal([[ -Local: ... (arg / arg, initial)]], get_value_info_as_string("")) +Local: ... (arg / arg)]], get_value_info_as_string("")) end) it("registers values in assignments correctly", function() assert.equal([[ -Local: ... (arg / arg, initial) -Local: a (var / var, initial) +Local: ... (arg / arg) +Local: a (var / var) Set: a (var / var)]], get_value_info_as_string([[ local a = b a = d]])) @@ -367,8 +363,8 @@ it("registers empty values correctly", function() assert.equal([[ -Local: ... (arg / arg, initial) -Local: a (var / var, initial), b (var / var, empty) +Local: ... (arg / arg) +Local: a (var / var), b (var / var, empty) Set: a (var / var), b (var / var)]], get_value_info_as_string([[ local a, b = 4 a, b = 5]])) @@ -376,23 +372,23 @@ 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: ... (arg / arg) +Local: f (var / func)]], 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) +Local: ... (arg / arg) +Local: i (loopi / loopi) 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) +Local: ... (arg / arg) +Local: a (var / var), b (var / var, 2 secondaries), c (var / var, 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()]])) @@ -400,7 +396,7 @@ 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: ... (arg / arg) Local: a (var / var, empty) Set: a (var / var, 1 secondaries, used)]], get_value_info_as_string([[ local a diff -Nru luacheck-0.22.0/spec/luacheck_spec.lua luacheck-0.23.0/spec/luacheck_spec.lua --- luacheck-0.22.0/spec/luacheck_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/luacheck_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -71,7 +71,6 @@ { code = "111", name = "embrace", - indexing = {"embrace"}, top = true }, { @@ -80,8 +79,7 @@ }, { code = "113", - name = "hepler", - indexing = {"hepler"} + name = "hepler" } }, { @@ -107,7 +105,6 @@ { code = "111", name = "embrace", - indexing = {"embrace"}, top = true }, { @@ -116,8 +113,7 @@ }, { code = "113", - name = "hepler", - indexing = {"hepler"} + name = "hepler" } }, { @@ -145,13 +141,11 @@ { code = "111", name = "embrace", - indexing = {"embrace"}, top = true }, { code = "113", - name = "hepler", - indexing = {"hepler"} + name = "hepler" } }, { @@ -210,8 +204,7 @@ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } }, { @@ -274,7 +267,7 @@ code = "511", line = 9, column = 1, - end_column = 1 + end_column = 5 } }, warnings = 4, @@ -306,14 +299,14 @@ code = "023", line = 3, column = 4, - end_column = 26 + end_column = 19 }, { code = "021", msg = "unknown inline option 'some invalid comment'", - line = 6, - column = 10, - end_column = 14 + line = 7, + column = 3, + end_column = 35 } }, warnings = 0, @@ -322,12 +315,11 @@ }, luacheck.check_strings({[[ -- luacheck: push local function f() - --[=[ luacheck: pop ]=] + -- luacheck: pop end -return f --[=[ - luacheck: some invalid comment -]=] +return f + -- luacheck: some invalid comment ]]})) end) @@ -356,8 +348,8 @@ code = "011", msg = "expected 'then' near ", line = 1, - column = 9, - end_column = 9 + column = 8, + end_column = 8 } }, { @@ -423,8 +415,7 @@ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } }, { @@ -448,7 +439,7 @@ end) it("returns a table with single error event on syntax error", function() - local report = strip_locations({luacheck.get_report("return return").events})[1] + local report = strip_locations({luacheck.get_report("return return").warnings})[1] assert.same({code = "011", msg = "expected expression near 'return'"}, report[1]) end) end) @@ -474,8 +465,7 @@ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } }, {}, @@ -491,15 +481,14 @@ { { code = "113", - name = "foo", - indexing = {"foo"} + name = "foo" } }, { { code = "113", name = "math", - indexing = {"math", "floor"} + indexing = {"floor"} } }, warnings = 2, diff -Nru luacheck-0.22.0/spec/parser_spec.lua luacheck-0.23.0/spec/parser_spec.lua --- luacheck-0.22.0/spec/parser_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/parser_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,21 +1,25 @@ +local decoder = require "luacheck.decoder" local 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]) +local function strip_locations(node) + node.line = nil + node.offset = nil + node.end_offset = nil + node.end_range = nil + + for _, sub_node in ipairs(node) do + if type(sub_node) == "table" then + strip_locations(sub_node) end end end +local function get_all(src_bytes) + return parser.parse(decoder.decode(src_bytes)) +end + local function get_ast(src) - local ast = parser.parse(src) + local ast = get_all(src) assert.is_table(ast) strip_locations(ast) return ast @@ -30,15 +34,19 @@ end local function get_comments(src) - return (select(2, parser.parse(src))) + return (select(2, get_all(src))) end local function get_code_lines(src) - return select(3, parser.parse(src)) + return select(3, get_all(src)) +end + +local function get_line_endings(src) + return select(4, get_all(src)) end local function get_error(src) - local ok, err = pcall(parser.parse, src) + local ok, err = pcall(get_all, src) assert.is_false(ok) return err end @@ -49,7 +57,7 @@ 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")) + assert.same({line = 1, offset = 1, end_offset = 3, msg = "expected near 'end'"}, get_error("end")) end) it("parses return statement correctly", function() @@ -62,7 +70,7 @@ {tag = "String", "foo"} }, get_node("return 1, 'foo'")) assert.same( - {line = 1, column = 10, end_column = 10, msg = "expected expression near "}, + {line = 1, offset = 10, end_offset = 10, msg = "expected expression near "}, get_error("return 1,") ) end) @@ -70,34 +78,34 @@ 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::")) + assert.same({line = 1, offset = 3, end_offset = 4, msg = "expected identifier near '::'"}, get_error("::::")) + assert.same({line = 1, offset = 3, end_offset = 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, offset = 5, end_offset = 5, msg = "expected identifier near "}, get_error("goto")) assert.same( - {line = 1, column = 9, end_column = 9, msg = "expected statement near ','"}, + {line = 1, offset = 9, end_offset = 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")) + assert.same({line = 1, offset = 11, end_offset = 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, prev_line = 1, prev_column = 1, prev_end_column = 2, + assert.same({line = 1, offset = 3, end_offset = 3, prev_line = 1, prev_offset = 1, prev_end_offset = 2, msg = "expected 'end' near "}, get_error("do")) - assert.same({line = 1, column = 4, end_column = 8, prev_line = 1, prev_column = 1, prev_end_column = 2, + assert.same({line = 1, offset = 4, end_offset = 8, prev_line = 1, prev_offset = 1, prev_end_offset = 2, msg = "expected 'end' near 'until'"}, get_error("do until false") ) - assert.same({line = 2, column = 1, end_column = 5, prev_line = 1, prev_column = 1, prev_end_column = 2, + assert.same({line = 2, offset = 4, end_offset = 8, prev_line = 1, prev_offset = 1, prev_end_offset = 2, msg = "expected 'end' (to close 'do' on line 1) near 'until'"}, get_error("do\nuntil false") ) @@ -108,22 +116,22 @@ {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, prev_line = 1, prev_column = 1, prev_end_column = 5, + assert.same({line = 1, offset = 6, end_offset = 6, msg = "expected condition near "}, get_error("while")) + assert.same({line = 1, offset = 11, end_offset = 11, msg = "expected 'do' near "}, get_error("while true")) + assert.same({line = 1, offset = 14, end_offset = 14, prev_line = 1, prev_offset = 1, prev_end_offset = 5, msg = "expected 'end' near "}, get_error("while true do") ) - assert.same({line = 2, column = 3, end_column = 3, prev_line = 1, prev_column = 1, prev_end_column = 5, + assert.same({line = 2, offset = 14, end_offset = 14, prev_line = 1, prev_offset = 1, prev_end_offset = 5, 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'"}, + {line = 1, offset = 7, end_offset = 8, msg = "expected condition near 'do'"}, get_error("while do end") ) assert.same( - {line = 1, column = 11, end_column = 11, msg = "expected 'do' near ','"}, + {line = 1, offset = 11, end_offset = 11, msg = "expected 'do' near ','"}, get_error("while true, false do end") ) end) @@ -133,19 +141,19 @@ {}, {tag = "True"} }, get_node("repeat until true")) - assert.same({line = 1, column = 7, end_column = 7, prev_line = 1, prev_column = 1, prev_end_column = 6, + assert.same({line = 1, offset = 7, end_offset = 7, prev_line = 1, prev_offset = 1, prev_end_offset = 6, msg = "expected 'until' near "}, get_error("repeat")) - assert.same({line = 3, column = 1, end_column = 1, prev_line = 1, prev_column = 1, prev_end_column = 6, + assert.same({line = 2, offset = 10, end_offset = 10, prev_line = 1, prev_offset = 1, prev_end_offset = 6, 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 "}, + {line = 1, offset = 13, end_offset = 13, msg = "expected condition near "}, get_error("repeat until") ) assert.same( - {line = 1, column = 18, end_column = 18, msg = "expected statement near ','"}, + {line = 1, offset = 18, end_offset = 18, msg = "expected statement near ','"}, get_error("repeat until true, false") ) end) @@ -156,21 +164,21 @@ {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, prev_line = 1, prev_column = 1, prev_end_column = 2, + assert.same({line = 1, offset = 3, end_offset = 3, msg = "expected condition near "}, get_error("if")) + assert.same({line = 1, offset = 8, end_offset = 8, msg = "expected 'then' near "}, get_error("if true")) + assert.same({line = 1, offset = 13, end_offset = 13, prev_line = 1, prev_offset = 1, prev_end_offset = 2, msg = "expected 'end' near "}, get_error("if true then") ) - assert.same({line = 2, column = 5, end_column = 5, prev_line = 1, prev_column = 1, prev_end_column = 2, + assert.same({line = 2, offset = 13, end_offset = 13, prev_line = 1, prev_offset = 1, prev_end_offset = 2, 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'"}, + {line = 1, offset = 4, end_offset = 7, msg = "expected condition near 'then'"}, get_error("if then end") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected 'then' near ','"}, + {line = 1, offset = 8, end_offset = 8, msg = "expected 'then' near ','"}, get_error("if true, false then end") ) end) @@ -181,15 +189,15 @@ {}, {} }, get_node("if true then else end")) - assert.same({line = 1, column = 18, end_column = 18, prev_line = 1, prev_column = 14, prev_end_column = 17, + assert.same({line = 1, offset = 18, end_offset = 18, prev_line = 1, prev_offset = 14, prev_end_offset = 17, msg = "expected 'end' near "}, get_error("if true then else") ) - assert.same({line = 3, column = 1, end_column = 1, prev_line = 2, prev_column = 6, prev_end_column = 9, + assert.same({line = 3, offset = 19, end_offset = 19, prev_line = 2, prev_offset = 14, prev_end_offset = 17, 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, prev_line = 1, prev_column = 14, prev_end_column = 17, + assert.same({line = 1, offset = 19, end_offset = 22, prev_line = 1, prev_offset = 14, prev_end_offset = 17, msg = "expected 'end' near 'else'"}, get_error("if true then else else end") ) @@ -203,14 +211,14 @@ {} }, get_node("if true then elseif false then end")) assert.same( - {line = 1, column = 21, end_column = 23, msg = "expected condition near 'end'"}, + {line = 1, offset = 21, end_offset = 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'"}, + {line = 1, offset = 21, end_offset = 24, msg = "expected condition near 'then'"}, get_error("if true then elseif then end") ) - assert.same({line = 2, column = 5, end_column = 5, prev_line = 1, prev_column = 14, prev_end_column = 19, + assert.same({line = 2, offset = 27, end_offset = 27, prev_line = 1, prev_offset = 14, prev_end_offset = 19, msg = "expected 'end' (to close 'elseif' on line 1) near "}, get_error("if true then elseif a\nthen") ) @@ -224,7 +232,7 @@ {}, {} }, get_node("if true then elseif false then else end")) - assert.same({line = 1, column = 36, end_column = 36, prev_line = 1, prev_column = 32, prev_end_column = 35, + assert.same({line = 1, offset = 36, end_offset = 36, prev_line = 1, prev_offset = 32, prev_end_offset = 35, msg = "expected 'end' near "}, get_error("if true then elseif false then else") ) @@ -240,35 +248,35 @@ {} }, get_node("for i=1, #t do end")) assert.same( - {line = 1, column = 4, end_column = 4, msg = "expected identifier near "}, + {line = 1, offset = 4, end_offset = 4, msg = "expected identifier near "}, get_error("for") ) assert.same( - {line = 1, column = 6, end_column = 6, msg = "expected '=', ',' or 'in' near "}, + {line = 1, offset = 6, end_offset = 6, msg = "expected '=', ',' or 'in' near "}, get_error("for i") ) assert.same( - {line = 1, column = 7, end_column = 8, msg = "expected '=', ',' or 'in' near '~='"}, + {line = 1, offset = 7, end_offset = 8, msg = "expected '=', ',' or 'in' near '~='"}, get_error("for i ~= 2") ) assert.same( - {line = 1, column = 11, end_column = 12, msg = "expected ',' near 'do'"}, + {line = 1, offset = 11, end_offset = 12, msg = "expected ',' near 'do'"}, get_error("for i = 2 do end") ) - assert.same({line = 1, column = 15, end_column = 15, prev_line = 1, prev_column = 1, prev_end_column = 3, + assert.same({line = 1, offset = 15, end_offset = 15, prev_line = 1, prev_offset = 1, prev_end_offset = 3, msg = "expected 'end' near "}, get_error("for i=1, #t do") ) - assert.same({line = 2, column = 4, end_column = 4, prev_line = 1, prev_column = 1, prev_end_column = 3, - msg = "expected 'end' (to close 'for' on line 1) near "}, + assert.same({line = 2, offset = 16, end_offset = 16, prev_line = 1, prev_offset = 1, prev_end_offset = 3, + msg = "expected 'end' (to close 'for' on line 1) near 'a' (indentation-based guess)"}, get_error("for i=1, #t do\na()") ) assert.same( - {line = 1, column = 5, end_column = 5, msg = "expected identifier near '('"}, + {line = 1, offset = 5, end_offset = 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'"}, + {line = 1, offset = 5, end_offset = 5, msg = "expected identifier near '3'"}, get_error("for 3=1, #t do end") ) end) @@ -282,7 +290,7 @@ {} }, get_node("for i=1, #t, 2 do end")) assert.same( - {line = 1, column = 15, end_column = 15, msg = "expected 'do' near ','"}, + {line = 1, offset = 15, end_offset = 15, msg = "expected 'do' near ','"}, get_error("for i=1, #t, 2, 3 do") ) end) @@ -305,11 +313,11 @@ {} }, get_node("for i, j in t, 'foo' do end")) assert.same( - {line = 1, column = 5, end_column = 6, msg = "expected identifier near 'in'"}, + {line = 1, offset = 5, end_offset = 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'"}, + {line = 1, offset = 10, end_offset = 11, msg = "expected expression near 'do'"}, get_error("for i in do end") ) end) @@ -324,39 +332,39 @@ } }, get_node("function a() end")) assert.same( - {line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, + {line = 1, offset = 9, end_offset = 9, msg = "expected identifier near "}, get_error("function") ) assert.same( - {line = 1, column = 11, end_column = 11, msg = "expected '(' near "}, + {line = 1, offset = 11, end_offset = 11, msg = "expected '(' near "}, get_error("function a") ) assert.same( - {line = 1, column = 12, end_column = 12, msg = "expected argument near "}, + {line = 1, offset = 12, end_offset = 12, msg = "expected argument near "}, get_error("function a(") ) - assert.same({line = 1, column = 13, end_column = 13, prev_line = 1, prev_column = 1, prev_end_column = 8, + assert.same({line = 1, offset = 13, end_offset = 13, prev_line = 1, prev_offset = 1, prev_end_offset = 8, msg = "expected 'end' near "}, get_error("function a()") ) - assert.same({line = 2, column = 2, end_column = 2, prev_line = 1, prev_column = 1, prev_end_column = 8, + assert.same({line = 2, offset = 14, end_offset = 14, prev_line = 1, prev_offset = 1, prev_end_offset = 8, 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 '('"}, + {line = 1, offset = 10, end_offset = 10, msg = "expected identifier near '('"}, get_error("function (a)()") ) assert.same( - {line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, + {line = 1, offset = 9, end_offset = 9, msg = "expected identifier near '('"}, get_error("function() end") ) assert.same( - {line = 1, column = 11, end_column = 11, msg = "expected '(' near 'a'"}, + {line = 1, offset = 11, end_offset = 11, msg = "expected '(' near 'a'"}, get_error("(function a() end)") ) assert.same( - {line = 1, column = 18, end_column = 18, msg = "expected expression near ')'"}, + {line = 1, offset = 18, end_offset = 18, msg = "expected expression near ')'"}, get_error("function a() end()") ) end) @@ -381,22 +389,22 @@ } }, get_node("function a(b, ...) end")) assert.same( - {line = 1, column = 15, end_column = 15, msg = "expected argument near ')'"}, + {line = 1, offset = 15, end_offset = 15, msg = "expected argument near ')'"}, get_error("function a(b, ) end") ) - assert.same({line = 1, column = 13, end_column = 13, prev_line = 1, prev_column = 11, prev_end_column = 11, + assert.same({line = 1, offset = 13, end_offset = 13, prev_line = 1, prev_offset = 11, prev_end_offset = 11, msg = "expected ')' near '.'"}, get_error("function a(b.c) end") ) - assert.same({line = 2, column = 2, end_column = 2, prev_line = 1, prev_column = 11, prev_end_column = 11, + assert.same({line = 2, offset = 14, end_offset = 14, prev_line = 1, prev_offset = 11, prev_end_offset = 11, 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 '('"}, + {line = 1, offset = 12, end_offset = 12, msg = "expected argument near '('"}, get_error("function a((b)) end") ) - assert.same({line = 1, column = 15, end_column = 15, prev_line = 1, prev_column = 11, prev_end_column = 11, + assert.same({line = 1, offset = 15, end_offset = 15, prev_line = 1, prev_offset = 11, prev_end_offset = 11, msg = "expected ')' near ','"}, get_error("function a(..., ...) end") ) @@ -419,11 +427,11 @@ } }, get_node("function a.b.c() end")) assert.same( - {line = 1, column = 11, end_column = 11, msg = "expected '(' near '['"}, + {line = 1, offset = 11, end_offset = 11, msg = "expected '(' near '['"}, get_error("function a[b]() end") ) assert.same( - {line = 1, column = 12, end_column = 12, msg = "expected identifier near '('"}, + {line = 1, offset = 12, end_offset = 12, msg = "expected identifier near '('"}, get_error("function a.() end") ) end) @@ -445,7 +453,7 @@ } }, get_node("function a.b:c() end")) assert.same( - {line = 1, column = 13, end_column = 13, msg = "expected '(' near '.'"}, + {line = 1, offset = 13, end_offset = 13, msg = "expected '(' near '.'"}, get_error("function a:b.c() end") ) end) @@ -463,75 +471,78 @@ } }, get_node("local a, b")) assert.same( - {line = 1, column = 6, end_column = 6, msg = "expected identifier near "}, + {line = 1, offset = 6, end_offset = 6, msg = "expected identifier near "}, get_error("local") ) assert.same( - {line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, + {line = 1, offset = 9, end_offset = 9, msg = "expected identifier near "}, get_error("local a,") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, + {line = 1, offset = 8, end_offset = 8, msg = "expected statement near '.'"}, get_error("local a.b") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, + {line = 1, offset = 8, end_offset = 8, msg = "expected statement near '['"}, get_error("local a[b]") ) assert.same( - {line = 1, column = 7, end_column = 7, msg = "expected identifier near '('"}, + {line = 1, offset = 7, end_offset = 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({ + 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 "}, + {line = 1, offset = 11, end_offset = 11, msg = "expected expression near "}, get_error("local a = ") ) assert.same( - {line = 1, column = 13, end_column = 13, msg = "expected expression near "}, + {line = 1, offset = 13, end_offset = 13, msg = "expected expression near "}, get_error("local a = b,") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, + {line = 1, offset = 8, end_offset = 8, msg = "expected statement near '.'"}, get_error("local a.b = c") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, + {line = 1, offset = 8, end_offset = 8, msg = "expected statement near '['"}, get_error("local a[b] = c") ) assert.same( - {line = 1, column = 10, end_column = 10, msg = "expected identifier near '('"}, + {line = 1, offset = 10, end_offset = 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({ + 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 "}, + {line = 1, offset = 15, end_offset = 15, msg = "expected identifier near "}, get_error("local function") ) assert.same( - {line = 1, column = 17, end_column = 17, msg = "expected '(' near '.'"}, + {line = 1, offset = 17, end_offset = 17, msg = "expected '(' near '.'"}, get_error("local function a.b() end") ) end) @@ -539,77 +550,84 @@ 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")) + 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 = "Paren", + {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, offset = 2, end_offset = 2, msg = "expected '=' near "}, get_error("a")) + assert.same({line = 1, offset = 5, end_offset = 5, msg = "expected expression near "}, get_error("a = ")) + assert.same({line = 1, offset = 5, end_offset = 5, msg = "expected statement near '='"}, get_error("a() = b")) + assert.same({line = 1, offset = 1, end_offset = 1, msg = "expected statement near '('"}, get_error("(a) = b")) + assert.same({line = 1, offset = 1, end_offset = 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({ + 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 "}, + {line = 1, offset = 5, end_offset = 5, msg = "expected '=' near "}, get_error("a, b") ) assert.same( - {line = 1, column = 4, end_column = 4, msg = "expected identifier or field near '='"}, + {line = 1, offset = 4, end_offset = 4, msg = "expected identifier or field near '='"}, get_error("a, = b") ) assert.same( - {line = 1, column = 8, end_column = 8, msg = "expected expression near "}, + {line = 1, offset = 8, end_offset = 8, msg = "expected expression near "}, get_error("a, b = ") ) assert.same( - {line = 1, column = 10, end_column = 10, msg = "expected expression near "}, + {line = 1, offset = 10, end_offset = 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 '='"}, + {line = 1, offset = 8, end_offset = 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 '('"}, + {line = 1, offset = 4, end_offset = 4, msg = "expected identifier or field near '('"}, get_error("a, (b) = c") ) end) @@ -617,17 +635,20 @@ 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"} + }, 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"} @@ -638,30 +659,30 @@ {tag = "Id", "c"} }, get_node("a(b, c)")) assert.same({tag = "Call", - {tag = "Id", "a"}, + {tag = "Paren", {tag = "Id", "a"}}, {tag = "Id", "b"} }, get_node("(a)(b)")) assert.same({tag = "Call", {tag = "Call", - {tag = "Id", "a"}, + {tag = "Paren", {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, prev_line = 1, prev_column = 2, prev_end_column = 2, + assert.same({line = 1, offset = 2, end_offset = 2, msg = "expected expression near ')'"}, get_error("()()")) + assert.same({line = 1, offset = 3, end_offset = 3, msg = "expected expression near "}, get_error("a(")) + assert.same({line = 1, offset = 4, end_offset = 4, prev_line = 1, prev_offset = 2, prev_end_offset = 2, msg = "expected ')' near "}, get_error("a(b")) - assert.same({line = 2, column = 2, end_column = 2, prev_line = 1, prev_column = 2, prev_end_column = 2, + assert.same({line = 2, offset = 5, end_offset = 5, prev_line = 1, prev_offset = 2, prev_end_offset = 2, msg = "expected ')' (to close '(' on line 1) near "}, get_error("a(\nb")) - assert.same({line = 2, column = 1, end_column = 2, prev_line = 1, prev_column = 1, prev_end_column = 1, + assert.same({line = 2, offset = 4, end_offset = 5, prev_line = 1, prev_offset = 1, prev_end_offset = 1, msg = "expected ')' (to close '(' on line 1) near 'cc'"}, get_error("(a\ncc")) - 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''"}, + assert.same({line = 1, offset = 1, end_offset = 1, msg = "expected statement near '1'"}, get_error("1()")) + assert.same({line = 1, offset = 1, end_offset = 5, msg = "expected statement near ''foo''"}, get_error("'foo'()")) - assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, + assert.same({line = 1, offset = 9, end_offset = 9, msg = "expected identifier near '('"}, get_error("function() end ()")) end) @@ -692,7 +713,7 @@ {tag = "Id", "d"} }, get_node("a:b(c, d)")) assert.same({tag = "Invoke", - {tag = "Id", "a"}, + {tag = "Paren", {tag = "Id", "a"}}, {tag = "String", "b"}, {tag = "Id", "c"} }, get_node("(a):b(c)")) @@ -702,13 +723,13 @@ {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 '('"}, + assert.same({line = 1, offset = 1, end_offset = 1, msg = "expected statement near '1'"}, get_error("1:b()")) + assert.same({line = 1, offset = 1, end_offset = 2, msg = "expected statement near ''''"}, get_error("'':a()")) + assert.same({line = 1, offset = 9, end_offset = 9, msg = "expected identifier near '('"}, get_error("function()end:b()")) - assert.same({line = 1, column = 4, end_column = 4, msg = "expected method arguments near ':'"}, + assert.same({line = 1, offset = 4, end_offset = 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:")) + assert.same({line = 1, offset = 3, end_offset = 3, msg = "expected identifier near "}, get_error("a:")) end) end) @@ -755,62 +776,28 @@ {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c;}")) - assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near ';'"}, + assert.same({line = 1, offset = 9, end_offset = 9, msg = "expected expression near ';'"}, get_error("return {;}")) - assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near "}, + assert.same({line = 1, offset = 9, end_offset = 9, msg = "expected expression near "}, get_error("return {")) - assert.same({line = 1, column = 11, end_column = 13, prev_line = 1, prev_column = 8, prev_end_column = 8, + assert.same({line = 1, offset = 11, end_offset = 13, prev_line = 1, prev_offset = 8, prev_end_offset = 8, msg = "expected '}' near 'end'"}, get_error("return {a end")) - assert.same({line = 2, column = 1, end_column = 3, prev_line = 1, prev_column = 8, prev_end_column = 8, + assert.same({line = 2, offset = 11, end_offset = 13, prev_line = 1, prev_offset = 8, prev_end_offset = 8, msg = "expected '}' (to close '{' on line 1) near 'end'"}, get_error("return {a\nend")) - assert.same({line = 1, column = 11, end_column = 11, prev_line = 1, prev_column = 9, prev_end_column = 9, + assert.same({line = 1, offset = 11, end_offset = 11, prev_line = 1, prev_offset = 9, prev_end_offset = 9, msg = "expected ']' near "}, get_error("return {[a")) - assert.same({line = 2, column = 2, end_column = 2, prev_line = 1, prev_column = 9, prev_end_column = 9, + assert.same({line = 2, offset = 12, end_offset = 12, prev_line = 1, prev_offset = 9, prev_end_offset = 9, 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 ','"}, + assert.same({line = 1, offset = 11, end_offset = 11, msg = "expected expression near ','"}, get_error("return {a,,}")) - assert.same({line = 1, column = 13, end_column = 13, msg = "expected expression near "}, + assert.same({line = 1, offset = 13, end_offset = 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"} @@ -889,43 +876,6 @@ } }, 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() @@ -939,97 +889,97 @@ end) it("does not allow statements after return", function() - assert.same({line = 1, column = 8, end_column = 12, msg = "expected expression near 'break'"}, + assert.same({line = 1, offset = 8, end_offset = 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'"}, + assert.same({line = 1, offset = 9, end_offset = 13, msg = "expected near 'break'"}, get_error("return; break")) - assert.same({line = 1, column = 8, end_column = 8, msg = "expected end of block near ';'"}, + assert.same({line = 1, offset = 8, end_offset = 8, msg = "expected near ';'"}, get_error("return;;")) - assert.same({line = 1, column = 10, end_column = 14, msg = "expected end of block near 'break'"}, + assert.same({line = 1, offset = 10, end_offset = 14, msg = "expected near 'break'"}, get_error("return 1 break")) - assert.same({line = 1, column = 11, end_column = 15, msg = "expected end of block near 'break'"}, + assert.same({line = 1, offset = 11, end_offset = 15, msg = "expected near 'break'"}, get_error("return 1; break")) - assert.same({line = 1, column = 13, end_column = 17, msg = "expected end of block near 'break'"}, + assert.same({line = 1, offset = 13, end_offset = 17, msg = "expected near 'break'"}, get_error("return 1, 2 break")) - assert.same({line = 1, column = 14, end_column = 18, msg = "expected end of block near 'break'"}, + assert.same({line = 1, offset = 14, end_offset = 18, msg = "expected 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 = "Localrec", + {{tag = "Id", "f"}}, + {{tag = "Function", {}, { + {tag = "While", + {tag = "True"}, + { + {tag = "If", + {tag = "Nil"}, + { + {tag = "Call", + {tag = "Id", "f"} }, - {tag = "Label", "fail"} - }} - }, - {tag = "Do", - {tag = "Fornum", - {tag = "Id", "i"}, - {tag = "Number", "1"}, - {tag = "Number", "2"}, - { - {tag = "Call", - {tag = "Id", "nothing"} - } - } + {tag = "Return"} }, - {tag = "Forin", - { - {tag = "Id", "k"}, - {tag = "Id", "v"} + {tag = "False"}, + { + {tag = "Call", + {tag = "Id", "g"} }, - { - {tag = "Call", - {tag = "Id", "pairs"} - } + {tag = "Break"} + }, + { + {tag = "Call", + {tag = "Id", "h"} }, - { - {tag = "Call", - {tag = "Id", "print"}, - {tag = "String", "bar"} + {tag = "Repeat", + { + {tag = "Goto", "fail"} }, - {tag = "Call", - {tag = "Id", "assert"}, - {tag = "Number", "42"} - } + {tag = "Id", "get_forked"} } - }, - {tag = "Return"} - }, - }, get_ast([[ + } + } + } + }, + {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 @@ -1067,74 +1017,311 @@ end) end) + describe("indentation-based missing until/end location guessing", function() + it("provides a better location on the same indentation level for missing end", function() + assert.same({line = 11, offset = 145, end_offset = 150, prev_line = 2, prev_offset = 23, prev_end_offset = 24, + msg = "expected 'end' (to close 'if' on line 2) near 'whoops' (indentation-based guess)"}, get_error([[ +local function f() + if cond then + do_thing() + + do_more_things() + + while true do + things_keep_happening() + end + + whoops() +end +]])) + + assert.same({line = 10, offset = 131, end_offset = 136, prev_line = 7, prev_offset = 84, prev_end_offset = 89, + msg = "expected 'until' (to close 'repeat' on line 7) near 'whoops' (indentation-based guess)" + }, get_error([[ +local function f() + if cond then + do_thing() + + do_more_things() + + repeat + things_keep_happening() + + whoops() +end +]])) + assert.same({line = 8, offset = 64, end_offset = 68, prev_line = 5, prev_offset = 41, prev_end_offset = 48, + msg = "expected 'end' (to close 'function' on line 5) near 'local' (indentation-based guess)" + }, get_error([[ +local function f() + good() +end + +local function g() + bad() + +local function t() + irrelevant() +end +]])) + + assert.same({line = 9, offset = 56, end_offset = 65, prev_line = 4, prev_offset = 15, prev_end_offset = 16, + msg = "expected 'end' (to close 'do' on line 4) near 'two_things' (indentation-based guess)" + }, get_error([[ +do end +do +end +do + do end + do + end + one_thing() +two_things() +]])) + + assert.same({line = 8, offset = 91, end_offset = 92, prev_line = 3, prev_offset = 16, prev_end_offset = 20, + msg = "expected 'end' (to close 'while' on line 3) near 'if' (indentation-based guess)" + }, get_error([[ +do + do + while cond + do + thing = thing + another = thing + + if yes then end + end +end +]])) + + assert.same({line = 6, offset = 117, end_offset = 125, prev_line = 3, prev_offset = 74, prev_end_offset = 76, + msg = "expected 'end' (to close 'for' on line 3) near 'something' (indentation-based guess)" + }, get_error([[ +function g() + for i in ipairs("this is not even an error...") do + for i = 1, 2, 3 do + thing() + + something = smth + end +]])) + end) + + it("provides a better location on a lower indentation level for missing end", function() + assert.same({line = 5, offset = 36, end_offset = 38, prev_line = 2, prev_offset = 7, prev_end_offset = 11, + msg = "expected 'end' (to close 'while' on line 2) near less indented 'end' (indentation-based guess)" + }, get_error([[ +do + while true do + thing() + +end +]])) + + assert.same({line = 5, offset = 51, end_offset = 51, prev_line = 2, prev_offset = 7, prev_end_offset = 11, + msg = "expected 'end' (to close 'while' on line 2) near 'a' (indentation-based guess)" + }, get_error([[ +do + while true do + thing() + more() +a() +]])) + end) + + it("provides a better location for various configurations of if statements", function() + assert.same({line = 6, offset = 67, end_offset = 69, prev_line = 2, prev_offset = 7, prev_end_offset = 8, + msg = "expected 'end' (to close 'if' on line 2) near less indented 'end' (indentation-based guess)" + }, get_error([[ +do + if thing({ +long, long, long, line}) then + something() + +end +]])) + + assert.same({line = 7, offset = 66, end_offset = 66, prev_line = 4, prev_offset = 43, prev_end_offset = 46, + msg = "expected 'end' (to close 'else' on line 4) near 'a' (indentation-based guess)" + }, get_error([[ +do + if cond() then + something() + else + thing() + + a = b +end +]])) + + assert.same({line = 6, offset = 66, end_offset = 68, prev_line = 4, prev_offset = 43, prev_end_offset = 48, + msg = "expected 'end' (to close 'elseif' on line 4) near less indented 'end' (indentation-based guess)" + }, get_error([[ +do + if cond() then + something() + elseif something then + +end +]])) + + assert.same({line = 10, offset = 119, end_offset = 119, prev_line = 8, prev_offset = 99, prev_end_offset = 104, + msg = "expected 'end' (to close 'elseif' on line 8) near 'e' (indentation-based guess)" + }, get_error([[ +do + if cond() then + s() + elseif something then + b() + elseif a() then + c() + elseif d() then + + e() +end +]])) + end) + + it("reports the first guess location outside complete blocks", function() + assert.same({line = 12, offset = 92, end_offset = 98, prev_line = 10, prev_offset = 61, prev_end_offset = 65, + msg = "expected 'end' (to close 'while' on line 10) near 'another' (indentation-based guess)" + }, get_error([[ +do + while true do + thing() + +another() +end +end + +do + while true do + thing() + another() +end + +do + while true do + thing() + another() +end +]])) + end) + + it("does not report blocks with different closing token comparing to original error", function() + assert.same({line = 10, offset = 87, end_offset = 91, prev_line = 8, prev_offset = 60, prev_end_offset = 65, + msg = "expected 'until' (to close 'repeat' on line 8) near less indented 'until' (indentation-based guess)" + }, get_error([[ +do + while true do + thing() + + a() + + repeat + repeat + thing() + until cond +end +]])) + + assert.same({line = 8, offset = 58, end_offset = 63, prev_line = 5, prev_offset = 30, prev_end_offset = 31, + msg = "expected 'end' (to close 'do' on line 5) near 'thing3' (indentation-based guess)" + }, get_error([[ +repeat +thing1() + + do + do + thing2() + + thing3() + end +until another_thing +]])) + end) + + it("does not report tokens on the same line as the innermost block opening token", function() + assert.same({line = 6, offset = 78, end_offset = 80, prev_line = 3, prev_offset = 60, prev_end_offset = 61, + msg = "expected 'end' (to close 'do' on line 3) near less indented 'end' (indentation-based guess)" + }, get_error([[ +local function f() + local function g() return ret end + do + thing() + +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 = "Localrec", line = 1, offset = 1, end_offset = 80, + {{tag = "Id", "foo", line = 1, offset = 16, end_offset = 18}}, + {{tag = "Function", line = 1, offset = 7, end_offset = 80, + end_range = {line = 4, offset = 78, end_offset = 80}, + { + {tag = "Id", "a", line = 1, offset = 20, end_offset = 20}, + {tag = "Id", "b", line = 1, offset = 23, end_offset = 23}, + {tag = "Id", "c", line = 1, offset = 26, end_offset = 26}, + {tag = "Dots", "...", line = 1, offset = 29, end_offset = 31} + }, + { + {tag = "Local", line = 2, offset = 37, end_offset = 57, + { + {tag = "Id", "d", line = 2, offset = 43, end_offset = 43} }, - {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}} - } - } - } + { + {tag = "Op", "mul", line = 2, offset = 47, end_offset = 57, + {tag = "Paren", line = 2, offset = 47, end_offset = 53, + {tag = "Op", "add", line = 2, offset = 48, end_offset = 52, + {tag = "Id", "a", line = 2, offset = 48, end_offset = 48}, + {tag = "Id", "b", line = 2, offset = 52, end_offset = 52} } + }, + {tag = "Id", "c", line = 2, offset = 57, end_offset = 57} + } + } + }, + {tag = "Return", line = 3, offset = 62, end_offset = 76, + {tag = "Id", "d", line = 3, offset = 69, end_offset = 69}, + {tag = "Paren", line = 3, offset = 72, end_offset = 76, + {tag = "Dots", "...", line = 3, offset = 73, end_offset = 75} + } + } + } + }} + }, + {tag = "Set", line = 6, offset = 83, end_offset = 144, + { + {tag = "Index", line = 6, offset = 92, end_offset = 96, + {tag = "Id", "t", line = 6, offset = 92, end_offset = 92}, + {tag = "String", "bar", line = 6, offset = 94, end_offset = 96} + } + }, + { + {tag = "Function", line = 6, offset = 83, end_offset = 144, + end_range = {line = 10, offset = 142, end_offset = 144}, + { + {tag = "Id", "self", implicit = true, line = 6, offset = 93, end_offset = 93}, + {tag = "Id", "arg", line = 6, offset = 98, end_offset = 100} + }, + { + {tag = "If", line = 7, offset = 106, end_offset = 140, + {tag = "Id", "arg", line = 7, offset = 109, end_offset = 111}, + {line = 7, offset = 113, end_offset = 116, -- Branch location. + {tag = "Call", line = 8, offset = 124, end_offset = 133, + {tag = "Id", "print", line = 8, offset = 124, end_offset = 128}, + {tag = "Id", "arg", line = 8, offset = 130, end_offset = 132} } } } - }, (parser.parse([[ + } + } + } + } + }, (get_all([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) @@ -1151,10 +1338,10 @@ 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.parse([[ + {tag = "Label", "foo", line = 1, offset = 1, end_offset = 7}, + {tag = "Label", "bar", line = 2, offset = 9, end_offset = 17}, + {tag = "Label", "baz", line = 3, offset = 18, end_offset = 25} + }, (get_all([[ ::foo:: :: bar :::: @@ -1164,28 +1351,33 @@ 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 = "Call", line = 1, offset = 1, end_offset = 3, + {tag = "Id", "a", line = 1, offset = 1, end_offset = 1} + }, + {tag = "Call", line = 2, offset = 6, end_offset = 10, + {tag = "Paren", line = 2, offset = 6, end_offset = 8, + {tag = "Id", "b", line = 2, offset = 7, end_offset = 7} + } + }, + {tag = "Set", line = 3, offset = 13, end_offset = 26, + { + {tag = "Index", line = 3, offset = 13, end_offset = 22, + {tag = "Paren", line = 3, offset = 13, end_offset = 19, + {tag = "Index", line = 3, offset = 14, end_offset = 18, + {tag = "Paren", line = 3, offset = 14, end_offset = 16, + {tag = "Id", "c", line = 3, offset = 15, end_offset = 15} }, - { - {tag = "Number", "2", location = {line = 3, column = 14, offset = 26}} - } + {tag = "String", "d", line = 3, offset = 18, end_offset = 18} } - }, (parser.parse([[ + }, + {tag = "Number", "3", line = 3, offset = 21, end_offset = 21} + } + }, + { + {tag = "Number", "2", line = 3, offset = 26, end_offset = 26} + } + } + }, (get_all([[ a(); (b)(); ((c).d)[3] = 2 @@ -1194,37 +1386,41 @@ it("provides correct location info for conditions", function() assert.same({ - {tag = "If", location = {line = 1, column = 1, offset = 1}, first_token = "if", - {tag = "Id", "x", location = {line = 1, column = 5, offset = 5}, first_token = "x"}, - {location = {line = 1, column = 8, offset = 8}} - } - }, (parser.parse([[ + {tag = "If", line = 1, offset = 1, end_offset = 15, + {tag = "Paren", line = 1, offset = 4, end_offset = 6, + {tag = "Id", "x", line = 1, offset = 5, end_offset = 5}, + }, + {line = 1, offset = 8, end_offset = 11} + } + }, (get_all([[ if (x) then end ]]))) end) it("provides correct location info for table keys", function() assert.same({ - {tag = "Return", location = {line = 1, column = 1, offset = 1}, first_token = "return", - {tag = "Table", location = {line = 1, column = 8, offset = 8}, - {tag = "Pair", location = {line = 1, column = 9, offset = 9}, first_token = "a", - {tag = "String", "a", location = {line = 1, column = 9, offset = 9}}, - {tag = "Id", "b", location = {line = 1, column = 13, offset = 13}} - }, - {tag = "Pair", location = {line = 1, column = 16, offset = 16}, first_token = "[", - {tag = "Id", "x", location = {line = 1, column = 17, offset = 17}}, - {tag = "Id", "y", location = {line = 1, column = 22, offset = 22}}, - }, - {tag = "Id", "z", location = {line = 1, column = 26, offset = 26}, first_token = "z"} - } - } - }, (parser.parse([[ + {tag = "Return", line = 1, offset = 1, end_offset = 28, + {tag = "Table", line = 1, offset = 8, end_offset = 28, + {tag = "Pair", line = 1, offset = 9, end_offset = 13, + {tag = "String", "a", line = 1, offset = 9, end_offset = 9}, + {tag = "Id", "b", line = 1, offset = 13, end_offset = 13} + }, + {tag = "Pair", line = 1, offset = 16, end_offset = 22, + {tag = "Id", "x", line = 1, offset = 17, end_offset = 17}, + {tag = "Id", "y", line = 1, offset = 22, end_offset = 22}, + }, + {tag = "Paren", line = 1, offset = 25, end_offset = 27, + {tag = "Id", "z", line = 1, offset = 26, end_offset = 26} + } + } + } + }, (get_all([[ return {a = b, [x] = y, (z)} ]]))) end) it("provides correct error location info", function() - assert.same({line = 8, column = 15, end_column = 15, msg = "expected '=' near ')'"}, get_error([[ + assert.same({line = 8, offset = 132, end_offset = 132, msg = "expected '=' near ')'"}, get_error([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) @@ -1238,12 +1434,17 @@ ]])) end) + it("provides correct error location info for EOF with no endline", function() + assert.same({line = 1, offset = 9, end_offset = 9, msg = "expected expression near "}, get_error("thing = ")) + assert.same( + {line = 1, offset = 15, end_offset = 15, msg = "expected expression near "}, get_error("thing = -- eof")) + end) + describe("providing misc information", function() - it("provides comments correctly", function() + it("provides short 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} + {contents = " ignore something", line = 1, offset = 1, end_offset = 19}, + {contents = " comments", line = 2, offset = 33, end_offset = 43} }, get_comments([[ -- ignore something foo = bar() -- comments @@ -1268,6 +1469,40 @@ } ::bar:: ]])) + assert.same({true}, get_code_lines("f() -- luacheck: ignore")) + end) + + it("provides line ending types correctly", function() + assert.same({ + "comment", + nil, + nil, + nil, + "string", + nil, + "comment", + "comment", + nil, + nil, + "string", + "string", + nil + }, get_line_endings([[ +-- comment +f() +--[=[comment]=] +f() +f("\ +string") +--[=[ + comment +]=] +f() +f([=[ + string +]=]) +]])) + assert.same({"comment"}, get_line_endings("f() -- comment")) end) end) end) diff -Nru luacheck-0.22.0/spec/projects/default_stds/default_stds-scm-1.rockspec luacheck-0.23.0/spec/projects/default_stds/default_stds-scm-1.rockspec --- luacheck-0.22.0/spec/projects/default_stds/default_stds-scm-1.rockspec 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/default_stds-scm-1.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,13 @@ +package = "default_stds" +version = "scm-1" +source = { + url = "https://example.com" +} +description = { + summary = "example", + detailed = "example", + homepage = "https://example.com", + license = "MIT" +} +dependencies = {} +it("is a rockspec")(newproxy, math, new_globals) diff -Nru luacheck-0.22.0/spec/projects/default_stds/.luacheckrc luacheck-0.23.0/spec/projects/default_stds/.luacheckrc --- luacheck-0.22.0/spec/projects/default_stds/.luacheckrc 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/.luacheckrc 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,14 @@ +std = "min" + +files["**/test/**/*_spec.lua"] = { + std = "none" +} + +local shared_options = {ignore = {"ignored"}} + +files["**/spec/**/*_spec.lua"] = shared_options +files["normal_file.lua"] = shared_options + +local function sink() end + +sink(it, version, math, newproxy) diff -Nru luacheck-0.22.0/spec/projects/default_stds/nested/spec/sample_spec.lua luacheck-0.23.0/spec/projects/default_stds/nested/spec/sample_spec.lua --- luacheck-0.22.0/spec/projects/default_stds/nested/spec/sample_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/nested/spec/sample_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,2 @@ +it("is a test in a nested directory")(newproxy, math, version, read_globals) +ignored() diff -Nru luacheck-0.22.0/spec/projects/default_stds/normal_file.lua luacheck-0.23.0/spec/projects/default_stds/normal_file.lua --- luacheck-0.22.0/spec/projects/default_stds/normal_file.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/normal_file.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,2 @@ +it("is just a normal file")(newproxy, math, version, read_globals) +ignored() diff -Nru luacheck-0.22.0/spec/projects/default_stds/sample_spec.lua luacheck-0.23.0/spec/projects/default_stds/sample_spec.lua --- luacheck-0.22.0/spec/projects/default_stds/sample_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/sample_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +it("is not really a test")(newproxy, math, version, read_globals) diff -Nru luacheck-0.22.0/spec/projects/default_stds/test/nested_normal_file.lua luacheck-0.23.0/spec/projects/default_stds/test/nested_normal_file.lua --- luacheck-0.22.0/spec/projects/default_stds/test/nested_normal_file.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/test/nested_normal_file.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +it("is a normal file in a nested directory")(newproxy, math, version, read_globals) diff -Nru luacheck-0.22.0/spec/projects/default_stds/test/sample_spec.lua luacheck-0.23.0/spec/projects/default_stds/test/sample_spec.lua --- luacheck-0.22.0/spec/projects/default_stds/test/sample_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/test/sample_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +it("is a test in a test directory")(newproxy, math, version, read_globals) diff -Nru luacheck-0.22.0/spec/projects/default_stds/tests/nested/sample_spec.lua luacheck-0.23.0/spec/projects/default_stds/tests/nested/sample_spec.lua --- luacheck-0.22.0/spec/projects/default_stds/tests/nested/sample_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/tests/nested/sample_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +it("is a test in a very nested directory")(newproxy, math, version, read_globals) diff -Nru luacheck-0.22.0/spec/projects/default_stds/tests/sample_spec.lua luacheck-0.23.0/spec/projects/default_stds/tests/sample_spec.lua --- luacheck-0.22.0/spec/projects/default_stds/tests/sample_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/projects/default_stds/tests/sample_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +it("is a test")(newproxy, math, version, read_globals) diff -Nru luacheck-0.22.0/spec/resolve_locals_spec.lua luacheck-0.23.0/spec/resolve_locals_spec.lua --- luacheck-0.22.0/spec/resolve_locals_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/resolve_locals_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,15 +1,14 @@ -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" +local helper = require "spec.helper" -local function used_variables_to_string(item) +local function used_variables_to_string(chstate, 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)) + table.insert(values_buf, ("%d:%d"):format( + value.var_node.line, chstate:offset_to_column(value.var_node.line, value.var_node.offset))) end table.insert(buf, var.name .. " = (" .. table.concat(values_buf, ", ") .. ")") @@ -20,17 +19,14 @@ end local function get_used_variables_as_string(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - resolve_locals(chstate) + local chstate = helper.get_chstate_after_stage("resolve_locals", src) local buf = {} - for _, item in ipairs(chstate.main_line.items) do + for _, item in ipairs(chstate.top_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)) + table.insert(buf, used_variables_to_string(chstate, item)) end end diff -Nru luacheck-0.22.0/spec/reversed_fornum_loops_spec.lua luacheck-0.23.0/spec/reversed_fornum_loops_spec.lua --- luacheck-0.22.0/spec/reversed_fornum_loops_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/reversed_fornum_loops_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,87 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_reversed_fornum_loops", src)) +end + +describe("reversed fornum loop detection", function() + it("does not detect anything wrong if not going down from #(expr)", function() + assert_warnings({}, [[ +for i = -10, 1 do + print(i) +end +]]) + end) + + it("does not detect anything wrong if limit may be greater than 1", function() + assert_warnings({}, [[ +for i = #t, 2 do + print(i) +end + +for i = #t, x do + print(i) +end +]]) + end) + + it("does not detect anything wrong if step may be negative", function() + assert_warnings({}, [[ +for i = #t, 1, -1 do + print(i) +end + +for i = #t, 1, x do + print(i) +end +]]) + end) + + it("detects reversed loops going from #(expr) to limit less than or equal to 1", function() + assert_warnings({ + {code = "571", line = 1, column = 1, end_column = 16, limit = "1"}, + {code = "571", line = 5, column = 1, end_column = 23, limit = "0"}, + {code = "571", line = 9, column = 1, end_column = 32, limit = "-123.456"} + }, [[ +for i = #t, 1 do + print(t[i]) +end + +for i = #"abcdef", 0 do + print(something) +end + +for i = #(...), -123.456, 567 do + print(something) +end +]]) + end) + + it("detects reversed loops in nested statements and functions", function() + assert_warnings({ + {code = "571", line = 7, column = 13, end_column = 28, limit = "1"}, + {code = "571", line = 8, column = 16, end_column = 31, limit = "1"}, + {code = "571", line = 10, column = 22, end_column = 43, limit = "1"} + }, [[ +do + print("thing") + + while true do + repeat + for i, v in ipairs(t) do + for i = #a, 1 do + for i = #b, 1 do + function xyz() + for i = #"thing", 1 do + print("thing") + end + end + end + end + end + until foo + end +end +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/rock/bin/rock.lua luacheck-0.23.0/spec/rock/bin/rock.lua --- luacheck-0.22.0/spec/rock/bin/rock.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/bin/rock.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock/bin/rock.sh luacheck-0.23.0/spec/rock/bin/rock.sh --- luacheck-0.22.0/spec/rock/bin/rock.sh 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/bin/rock.sh 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +# nothing diff -Nru luacheck-0.22.0/spec/rock/lua_modules/something.lua luacheck-0.23.0/spec/rock/lua_modules/something.lua --- luacheck-0.22.0/spec/rock/lua_modules/something.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/lua_modules/something.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock/rock-dev-1.rockspec luacheck-0.23.0/spec/rock/rock-dev-1.rockspec --- luacheck-0.22.0/spec/rock/rock-dev-1.rockspec 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/rock-dev-1.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,18 @@ +rockspec_format = "3.0" +package = "rock" +version = "dev-1" +source = { + url = "https://github.com/rockman/rock" +} +description = { + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} +test_dependencies = { + "busted = 2.0.rc12-1" +} +test = { + type = "busted" +} diff -Nru luacheck-0.22.0/spec/rock/src/rock/mod.lua luacheck-0.23.0/spec/rock/src/rock/mod.lua --- luacheck-0.22.0/spec/rock/src/rock/mod.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/src/rock/mod.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock/src/rock/thing.c luacheck-0.23.0/spec/rock/src/rock/thing.c --- luacheck-0.22.0/spec/rock/src/rock/thing.c 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/src/rock/thing.c 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +// nothing diff -Nru luacheck-0.22.0/spec/rock/src/rock.lua luacheck-0.23.0/spec/rock/src/rock.lua --- luacheck-0.22.0/spec/rock/src/rock.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/src/rock.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock/test.lua luacheck-0.23.0/spec/rock/test.lua --- luacheck-0.22.0/spec/rock/test.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock/test.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock2/mod.lua luacheck-0.23.0/spec/rock2/mod.lua --- luacheck-0.22.0/spec/rock2/mod.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock2/mod.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/rock2/rock2-dev-1.rockspec luacheck-0.23.0/spec/rock2/rock2-dev-1.rockspec --- luacheck-0.22.0/spec/rock2/rock2-dev-1.rockspec 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock2/rock2-dev-1.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "rock2" +version = "dev-1" +source = { + url = "https://github.com/rockman/rock2" +} +description = { + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} +test_dependencies = { + "busted = 2.0.rc12-1" +} +build = {} +test = { + type = "busted" +} diff -Nru luacheck-0.22.0/spec/rock2/spec/rock2_spec.lua luacheck-0.23.0/spec/rock2/spec/rock2_spec.lua --- luacheck-0.22.0/spec/rock2/spec/rock2_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/rock2/spec/rock2_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1 @@ +-- nothing diff -Nru luacheck-0.22.0/spec/samples/argparse-0.2.0.lua luacheck-0.23.0/spec/samples/argparse-0.2.0.lua --- luacheck-0.22.0/spec/samples/argparse-0.2.0.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/samples/argparse-0.2.0.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,973 @@ +local 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 diff -Nru luacheck-0.22.0/spec/samples/argparse.lua luacheck-0.23.0/spec/samples/argparse.lua --- luacheck-0.22.0/spec/samples/argparse.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/samples/argparse.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,973 +0,0 @@ -local 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 diff -Nru luacheck-0.22.0/spec/samples/bad.rockspec luacheck-0.23.0/spec/samples/bad.rockspec --- luacheck-0.22.0/spec/samples/bad.rockspec 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/samples/bad.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -1 +1 @@ -build = 0 +bad("???") \ No newline at end of file diff -Nru luacheck-0.22.0/spec/samples/reversed_fornum.lua luacheck-0.23.0/spec/samples/reversed_fornum.lua --- luacheck-0.22.0/spec/samples/reversed_fornum.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/samples/reversed_fornum.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,3 @@ +for i = #(...), -1.5 do + print(i) +end diff -Nru luacheck-0.22.0/spec/samples/sample.rockspec luacheck-0.23.0/spec/samples/sample.rockspec --- luacheck-0.22.0/spec/samples/sample.rockspec 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/samples/sample.rockspec 2018-09-18 19:43:27.000000000 +0000 @@ -2,6 +2,7 @@ type = "builtin", modules = { good = "spec/samples/good_code.lua", - bad = "spec/samples/bad_code.lua" + bad = "spec/samples/bad_code.lua", + not_even_a_module = some_global } } diff -Nru luacheck-0.22.0/spec/samples/utf8_error.lua luacheck-0.23.0/spec/samples/utf8_error.lua --- luacheck-0.22.0/spec/samples/utf8_error.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/samples/utf8_error.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,2 @@ +-- 嗨,你好嗎? +--[[GÆT]] ошибка =( diff -Nru luacheck-0.22.0/spec/samples/utf8.lua luacheck-0.23.0/spec/samples/utf8.lua --- luacheck-0.22.0/spec/samples/utf8.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/samples/utf8.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,8 @@ +-- 嗨,你好嗎? +math["분야 명"] = math["値"] +--[[комментарий]] local t = { + ["päällekkäinen nimi a​b"] = 1, + ["päällekkäinen nimi a​b"] = 2 +} + +-- líne an-fhada a choinníonn dul ag dul agus ag dul agus ag dul agus ag dul ach nach bhfuil 120 carachtar ann. Y diwed diff -Nru luacheck-0.22.0/spec/unbalanced_assignments_spec.lua luacheck-0.23.0/spec/unbalanced_assignments_spec.lua --- luacheck-0.22.0/spec/unbalanced_assignments_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/unbalanced_assignments_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,56 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_unbalanced_assignments", src)) +end + +describe("unbalanced assignment detection", function() + it("detects unbalanced assignments", function() + assert_warnings({ + {code = "532", line = 4, column = 1, end_column = 8}, + {code = "531", line = 5, column = 1, end_column = 14} + }, [[ +local a, b = 4; (...)(a) + +a, b = (...)(); (...)(a, b) +a, b = 5; (...)(a, b) +a, b = 1, 2, 3; (...)(a, b) +local c, d +]]) + end) + + it("detects unbalanced assignments in nested blocks and functions", function() + assert_warnings({ + {code = "532", line = 6, column = 10, end_column = 17}, + {code = "532", line = 9, column = 13, end_column = 20}, + {code = "532", line = 14, column = 22, end_column = 29}, + {code = "531", line = 17, column = 25, end_column = 38} + }, [[ +do + local a, b, c, d + + while x do + if y then + a, b = 1 + else + repeat + a, b = 1 + + function t() + for i = 1, 10 do + for _, v in ipairs(tab) do + a, b = 1 + + if c then + a, b = 1, 2, 3 + end + end + end + end + until z + end + end +end +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/uninit_accesses_spec.lua luacheck-0.23.0/spec/uninit_accesses_spec.lua --- luacheck-0.22.0/spec/uninit_accesses_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/uninit_accesses_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,125 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_uninit_accesses", src)) +end + +describe("uninitalized access detection", function() + it("detects accessing uninitialized variables", function() + assert_warnings({ + {code = "321", name = "a", line = 6, column = 12, end_column = 12} + }, [[ +local a + +if ... then + a = 5 +else + a = get(a) +end + +return a +]]) + end) + + it("detects accessing uninitialized variables in unreachable functions", function() + assert_warnings({ + {code = "321", name = "a", line = 12, column = 20, end_column = 20} + }, [[ +return function() + return function() + do return end + + return function(x) + local a + + if x then + a = 1 + return a + 2 + else + return a + 1 + end + end + end +end +]]) + end) + + it("detects mutating uninitialized variables", function() + assert_warnings({ + {code = "341", name = "a", line = 4, column = 4, end_column = 4} + }, [[ +local a + +if ... then + a.k = 5 +else + a = get(5) +end + +return a +]]) + end) + + it("detects accessing uninitialized variables in nested functions", function() + assert_warnings({ + {code = "321", name = "a", line = 7, column = 12, end_column = 12} + }, [[ +return function() return function(...) +local a + +if ... then + a = 5 +else + a = get(a) +end + +return a +end end +]]) + end) + + it("handles accesses with no reaching values", function() + assert_warnings({}, [[ +local var = "foo" +(...)(var) +do return end +(...)(var) +]]) + end) + + it("handles upvalue accesses with no reaching values", function() + assert_warnings({}, [[ +local var = "foo" +(...)(var) +do return end +(...)(function() + return var +end) +]]) + end) + + it("handles upvalue accesses with no reaching values in a nested function", function() + assert_warnings({}, [[ +return function(...) + local var = "foo" + (...)(var) + do return end + (...)(function() + return var + end) +end +]]) + end) + + it("does not detect accessing unitialized variables incorrectly in loops", function() + assert_warnings({}, [[ +local a + +while not a do + a = get() +end + +return a +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/uninit_access_spec.lua luacheck-0.23.0/spec/uninit_access_spec.lua --- luacheck-0.22.0/spec/uninit_access_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/uninit_access_spec.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,140 +0,0 @@ -local core_utils = require "luacheck.core_utils" -local detect_uninit_access = require "luacheck.detect_uninit_access" -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - resolve_locals(chstate) - chstate.warnings = {} - detect_uninit_access(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end - -local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) -end - -describe("uninitalized access detection", function() - it("detects accessing uninitialized variables", function() - assert_warnings({ - {code = "321", name = "a", line = 6, column = 12, end_column = 12} - }, [[ -local a - -if ... then - a = 5 -else - a = get(a) -end - -return a -]]) - end) - - it("detects accessing uninitialized variables in unreachable functions", function() - assert_warnings({ - {code = "321", name = "a", line = 12, column = 20, end_column = 20} - }, [[ -return function() - return function() - do return end - - return function(x) - local a - - if x then - a = 1 - return a + 2 - else - return a + 1 - end - end - end -end -]]) - end) - - it("detects mutating uninitialized variables", function() - assert_warnings({ - {code = "341", name = "a", line = 4, column = 4, end_column = 4} - }, [[ -local a - -if ... then - a.k = 5 -else - a = get(5) -end - -return a -]]) - end) - - it("detects accessing uninitialized variables in nested functions", function() - assert_warnings({ - {code = "321", name = "a", line = 7, column = 12, end_column = 12} - }, [[ -return function() return function(...) -local a - -if ... then - a = 5 -else - a = get(a) -end - -return a -end end -]]) - end) - - it("handles accesses with no reaching values", function() - assert_warnings({}, [[ -local var = "foo" -(...)(var) -do return end -(...)(var) -]]) - end) - - it("handles upvalue accesses with no reaching values", function() - assert_warnings({}, [[ -local var = "foo" -(...)(var) -do return end -(...)(function() - return var -end) -]]) - end) - - it("handles upvalue accesses with no reaching values in a nested function", function() - assert_warnings({}, [[ -return function(...) - local var = "foo" - (...)(var) - do return end - (...)(function() - return var - end) -end -]]) - end) - - it("does not detect accessing unitialized variables incorrectly in loops", function() - assert_warnings({}, [[ -local a - -while not a do - a = get() -end - -return a -]]) - end) -end) diff -Nru luacheck-0.22.0/spec/unreachable_code_spec.lua luacheck-0.23.0/spec/unreachable_code_spec.lua --- luacheck-0.22.0/spec/unreachable_code_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/unreachable_code_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,26 +1,13 @@ -local core_utils = require "luacheck.core_utils" -local detect_unreachable_code = require "luacheck.detect_unreachable_code" -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - chstate.warnings = {} - detect_unreachable_code(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end +local helper = require "spec.helper" local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) + assert.same(warnings, helper.get_stage_warnings("detect_unreachable_code", src)) end describe("unreachable code detection", function() it("detects unreachable code", function() assert_warnings({ - {code = "511", line = 2, column = 1, end_column = 2} + {code = "511", line = 2, column = 1, end_column = 24} }, [[ do return end if ... then return 6 end @@ -28,8 +15,8 @@ ]]) assert_warnings({ - {code = "511", line = 7, column = 1, end_column = 2}, - {code = "511", line = 13, column = 1, end_column = 6} + {code = "511", line = 7, column = 1, end_column = 11}, + {code = "511", line = 13, column = 1, end_column = 8} }, [[ if ... then return 4 @@ -83,7 +70,7 @@ {code = "511", line = 3, column = 7, end_column = 9} }, [[ repeat - return + return until ... ]]) @@ -100,7 +87,7 @@ it("detects unreachable functions", function() assert_warnings({ - {code = "511", line = 3, column = 1, end_column = 8} + {code = "511", line = 3, column = 1, end_column = 16} }, [[ local f = nil do return end @@ -123,7 +110,7 @@ it("detects unreachable code in unreachable nested function", function() assert_warnings({ - {code = "511", line = 4, column = 4, end_column = 9}, + {code = "511", line = 4, column = 4, end_column = 20}, {code = "511", line = 6, column = 7, end_column = 12} }, [[ return function() diff -Nru luacheck-0.22.0/spec/unused_fields_spec.lua luacheck-0.23.0/spec/unused_fields_spec.lua --- luacheck-0.22.0/spec/unused_fields_spec.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/spec/unused_fields_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,46 @@ +local helper = require "spec.helper" + +local function assert_warnings(warnings, src) + assert.same(warnings, helper.get_stage_warnings("detect_unused_fields", src)) +end + +describe("unused field detection", function() + it("detects unused fields in table literals", function() + assert_warnings({ + {code = "314", field = "key", line = 3, column = 5, end_column = 9, + overwritten_line = 7, overwritten_column = 4, overwritten_end_column = 6}, + {code = "314", field = "2", index = true, line = 6, column = 4, end_column = 4, + overwritten_line = 9, overwritten_column = 5, overwritten_end_column = 9}, + {code = "314", field = "key", line = 7, column = 4, end_column = 6, + overwritten_line = 8, overwritten_column = 4, overwritten_end_column = 6}, + {code = "314", field = "0.2e1", line = 9, column = 5, end_column = 9, + overwritten_line = 10, overwritten_column = 5, overwritten_end_column = 5} + }, [[ +local x, y, z = 1, 2, 3 +return { + ["key"] = 4, + [z] = 7, + 1, + y, + key = x, + key = 0, + [0.2e1] = 6, + [2] = 7 +} +]]) + end) + + it("detects unused fields in nested table literals", function() + assert_warnings({ + {code = "314", field = "a", line = 2, column = 5, end_column = 5, + overwritten_line = 2, overwritten_column = 12, overwritten_end_column = 12}, + {code = "314", field = "b", line = 3, column = 11, end_column = 11, + overwritten_line = 3, overwritten_column = 18, overwritten_end_column = 18} + }, [[ +return { + {a = 1, a = 2}, + key = {b = 1, b = 2} +} +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/unused_locals_spec.lua luacheck-0.23.0/spec/unused_locals_spec.lua --- luacheck-0.22.0/spec/unused_locals_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/unused_locals_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,22 +1,7 @@ -local core_utils = require "luacheck.core_utils" -local detect_unused_locals = require "luacheck.detect_unused_locals" -local linearize = require "luacheck.linearize" -local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - resolve_locals(chstate) - chstate.warnings = {} - detect_unused_locals(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end +local helper = require "spec.helper" local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) + assert.same(warnings, helper.get_stage_warnings("detect_unused_locals", src)) end describe("unused locals detection", function() @@ -26,7 +11,7 @@ local b = 5 a = 6 do - print(b, {a}) + print(b, {(a)}) end ]]) end) @@ -302,3 +287,86 @@ ]]) end) end) + +describe("unused recurisve function detection", function() + it("detects unused recursive functions", function() + assert_warnings({ + {code = "211", name = "f", func = true, recursive = true, line = 1, column = 16, end_column = 16} + }, [[ +local function f(x) + return x <= 1 and 1 or x * f(x - 1) +end +]]) + end) + + it("handles functions defined without a local value", function() + assert_warnings({}, [[ +print(function() return function() end end) +]]) + end) + + it("detects unused mutually recursive functions", function() + assert_warnings({ + {code = "211", name = "odd", func = true, mutually_recursive = true, line = 3, column = 16, end_column = 18}, + {code = "211", name = "even", func = true, mutually_recursive = true, line = 7, column = 10, end_column = 13} + }, [[ +local even + +local function odd(x) + return x == 1 or even(x - 1) +end + +function even(x) + return x == 0 or odd(x - 1) +end +]]) + end) + + it("detects unused mutually recursive functions as values", function() + assert_warnings({ + {code = "311", name = "odd", line = 5, column = 10, end_column = 12}, + {code = "311", name = "even", line = 9, column = 10, end_column = 13} + }, [[ +local even = 2 +local odd = 3 +(...)(even, odd) + +function odd(x) + return x == 1 or even(x - 1) +end + +function even(x) + return x == 0 or odd(x - 1) or even(x) +end +]]) + end) + + it("does not incorrectly detect unused recursive functions inside unused functions", function() + assert_warnings({ + {code = "211", name = "unused", func = true, line = 1, column = 16, end_column = 21} + }, [[ +local function unused() + local function nested1() end + local function nested2() nested2() end + return nested1(), nested2() +end +]]) + end) + + it("does not incorrectly detect unused recursive functions used by an unused recursive function", function() + assert_warnings({ + {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} + }, [[ +local function f() return 1 end +local function g() return f() + g() end +]]) + + assert_warnings({ + {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} + }, [[ +local f +local function g() return f() + g() end +function f() return 1 end +]]) + end) +end) diff -Nru luacheck-0.22.0/spec/unused_rec_funcs_spec.lua luacheck-0.23.0/spec/unused_rec_funcs_spec.lua --- luacheck-0.22.0/spec/unused_rec_funcs_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/unused_rec_funcs_spec.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -local core_utils = require "luacheck.core_utils" -local detect_unused_rec_funcs = require "luacheck.detect_unused_rec_funcs" -local linearize = require "luacheck.linearize" -local name_functions = require "luacheck.name_functions" -local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" - -local function get_warnings(src) - local ast = parser.parse(src) - local chstate = {ast = ast, warnings = {}} - linearize(chstate) - name_functions(chstate) - resolve_locals(chstate) - chstate.warnings = {} - detect_unused_rec_funcs(chstate) - core_utils.sort_by_location(chstate.warnings) - return chstate.warnings -end - -local function assert_warnings(warnings, src) - assert.same(warnings, get_warnings(src)) -end - -describe("unused recurisve function detection", function() - it("detects unused recursive functions", function() - assert_warnings({ - {code = "211", name = "f", func = true, recursive = true, line = 1, column = 16, end_column = 16} - }, [[ -local function f(x) - return x <= 1 and 1 or x * f(x - 1) -end -]]) - end) - - it("handles functions defined without a local value", function() - assert_warnings({}, [[ -print(function() return function() end end) -]]) - end) - - it("detects unused mutually recursive functions", function() - assert_warnings({ - {code = "211", name = "odd", func = true, mutually_recursive = true, line = 3, column = 16, end_column = 18}, - {code = "211", name = "even", func = true, mutually_recursive = true, line = 7, column = 10, end_column = 13} - }, [[ -local even - -local function odd(x) - return x == 1 or even(x - 1) -end - -function even(x) - return x == 0 or odd(x - 1) -end -]]) - end) - - it("detects unused mutually recursive functions as values", function() - assert_warnings({ - {code = "311", name = "odd", line = 5, column = 10, end_column = 12}, - {code = "311", name = "even", line = 9, column = 10, end_column = 13} - }, [[ -local even = 2 -local odd = 3 -(...)(even, odd) - -function odd(x) - return x == 1 or even(x - 1) -end - -function even(x) - return x == 0 or odd(x - 1) or even(x) -end -]]) - end) - - it("does not incorrectly detect unused recursive functions inside unused functions", function() - assert_warnings({}, [[ -local function unused() - local function nested1() end - local function nested2() nested2() end - return nested1(), nested2() -end -]]) - end) - - it("does not incorrectly detect unused recursive functions used by an unused recursive function", function() - assert_warnings({ - {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} - }, [[ -local function f() return 1 end -local function g() return f() + g() end -]]) - - assert_warnings({ - {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} - }, [[ -local f -local function g() return f() + g() end -function f() return 1 end -]]) - end) -end) diff -Nru luacheck-0.22.0/spec/utils_spec.lua luacheck-0.23.0/spec/utils_spec.lua --- luacheck-0.22.0/spec/utils_spec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/spec/utils_spec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -216,15 +216,6 @@ end) end) - describe("split_lines", function() - it("considers \\n, \\r, \\r\\n, and \\n\\r line endings", function() - assert.same( - {"foo", "", "bar", "baz", "", "quux", "line ", "another one"}, - utils.split_lines("foo\n\nbar\r\nbaz\r\rquux\n\rline \nanother one") - ) - end) - end) - describe("map", function() it("maps function over an array", function() assert.same({3, 1, 2}, utils.map(math.sqrt, {9, 1, 4})) diff -Nru luacheck-0.22.0/src/luacheck/argparse.lua luacheck-0.23.0/src/luacheck/argparse.lua --- luacheck-0.22.0/src/luacheck/argparse.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/argparse.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,1527 +0,0 @@ --- The MIT License (MIT) - --- Copyright (c) 2013 - 2018 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 = {}, - _groups = {}, - _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"), - typechecked("command_target", "string"), - typechecked("help_vertical_space", "number"), - typechecked("usage_margin", "number"), - typechecked("usage_max_width", "number"), - typechecked("help_usage_margin", "number"), - typechecked("help_description_margin", "number"), - typechecked("help_max_width", "number"), - 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"), - typechecked("command_target", "string"), - typechecked("help_vertical_space", "number"), - typechecked("usage_margin", "number"), - typechecked("usage_max_width", "number"), - typechecked("help_usage_margin", "number"), - typechecked("help_description_margin", "number"), - typechecked("help_max_width", "number"), - typechecked("hidden", "boolean"), - 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"), - typechecked("hidden", "boolean"), - 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"), - typechecked("hidden", "boolean"), - option_action, - option_init -}, Argument) - -function Parser:_inherit_property(name, default) - local element = self - - while true do - local value = element["_" .. name] - - if value ~= nil then - return value - end - - if not element._parent then - return default - end - - element = element._parent - end -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) - - 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 labels to be shown in the help message. -function Argument:_get_label_lines() - return {self._name} -end - -function Option:_get_label_lines() - local argument_list = self:_get_argument_list() - - if #argument_list == 0 then - -- Don't put aliases for simple flags like `-h` on different lines. - return {table.concat(self._aliases, ", ")} - end - - local longest_alias_length = -1 - - for _, alias in ipairs(self._aliases) do - longest_alias_length = math.max(longest_alias_length, #alias) - end - - local argument_list_repr = table.concat(argument_list, " ") - local lines = {} - - for i, alias in ipairs(self._aliases) do - local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr - - if i ~= #self._aliases then - line = line .. "," - end - - table.insert(lines, line) - end - - return lines -end - -function Command:_get_label_lines() - 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 elements = {...} - - for i, element in ipairs(elements) do - local mt = getmetatable(element) - assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) - end - - table.insert(self._mutexes, elements) - return self -end - -function Parser:group(name, ...) - assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) - - local group = {name = name, ...} - - for i, element in ipairs(group) do - local mt = getmetatable(element) - assert(mt == Option or mt == Argument or mt == Command, - ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) - end - - table.insert(self._groups, group) - return self -end - -local usage_welcome = "Usage: " - -function Parser:get_usage() - if self._usage then - return self._usage - end - - local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) - local max_usage_width = self:_inherit_property("usage_max_width", 70) - 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_margin) .. s - end - end - - -- Normally options are before positional arguments in usage messages. - -- However, vararg options should be after, because they can't be reliable used - -- before a positional argument. - -- Mutexes come into play, too, and are shown as soon as possible. - -- Overall, output usages in the following order: - -- 1. Mutexes that don't have positional arguments or vararg options. - -- 2. Options that are not in any mutexes and are not vararg. - -- 3. Positional arguments - on their own or as a part of a mutex. - -- 4. Remaining mutexes. - -- 5. Remaining options. - - local elements_in_mutexes = {} - local added_elements = {} - local added_mutexes = {} - local argument_to_mutexes = {} - - local function add_mutex(mutex, main_argument) - if added_mutexes[mutex] then - return - end - - added_mutexes[mutex] = true - local buf = {} - - for _, element in ipairs(mutex) do - if not element._hidden and not added_elements[element] then - if getmetatable(element) == Option or element == main_argument then - table.insert(buf, element:_get_usage()) - added_elements[element] = true - end - end - end - - if #buf == 1 then - add(buf[1]) - elseif #buf > 1 then - add("(" .. table.concat(buf, " | ") .. ")") - end - end - - local function add_element(element) - if not element._hidden and not added_elements[element] then - add(element:_get_usage()) - added_elements[element] = true - end - end - - for _, mutex in ipairs(self._mutexes) do - local is_vararg = false - local has_argument = false - - for _, element in ipairs(mutex) do - if getmetatable(element) == Option then - if element:_is_vararg() then - is_vararg = true - end - else - has_argument = true - argument_to_mutexes[element] = argument_to_mutexes[element] or {} - table.insert(argument_to_mutexes[element], mutex) - end - - elements_in_mutexes[element] = true - end - - if not is_vararg and not has_argument then - add_mutex(mutex) - end - end - - for _, option in ipairs(self._options) do - if not elements_in_mutexes[option] and not option:_is_vararg() then - add_element(option) - end - end - - -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. - for _, argument in ipairs(self._arguments) do - -- Pick a mutex as a part of which to show this argument, take the first one that's still available. - local mutex - - if elements_in_mutexes[argument] then - for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do - if not added_mutexes[argument_mutex] then - mutex = argument_mutex - end - end - end - - if mutex then - add_mutex(mutex, argument) - else - add_element(argument) - end - end - - for _, mutex in ipairs(self._mutexes) do - add_mutex(mutex) - end - - for _, option in ipairs(self._options) do - add_element(option) - end - - if #self._commands > 0 then - if self._require_command then - add("") - else - add("[]") - end - - add("...") - end - - return table.concat(lines, "\n") -end - -local function split_lines(s) - if s == "" then - return {} - end - - local lines = {} - - if s:sub(-1) ~= "\n" then - s = s .. "\n" - end - - for line in s:gmatch("([^\n]*)\n") do - table.insert(lines, line) - end - - return lines -end - -local function autowrap_line(line, max_length) - -- Algorithm for splitting lines is simple and greedy. - local result_lines = {} - - -- Preserve original indentation of the line, put this at the beginning of each result line. - -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts - -- of the second and the following lines vertically align with the start of the second word. - local indentation = line:match("^ *") - - if line:find("^ *[%*%+%-]") then - indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") - end - - -- Parts of the last line being assembled. - local line_parts = {} - - -- Length of the current line. - local line_length = 0 - - -- Index of the next character to consider. - local index = 1 - - while true do - local word_start, word_finish, word = line:find("([^ ]+)", index) - - if not word_start then - -- Ignore trailing spaces, if any. - break - end - - local preceding_spaces = line:sub(index, word_start - 1) - index = word_finish + 1 - - if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then - -- Either this is the very first word or it fits as an addition to the current line, add it. - table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. - table.insert(line_parts, word) - line_length = line_length + #preceding_spaces + #word - else - -- Does not fit, finish current line and put the word into a new one. - table.insert(result_lines, table.concat(line_parts)) - line_parts = {indentation, word} - line_length = #indentation + #word - end - end - - if #line_parts > 0 then - table.insert(result_lines, table.concat(line_parts)) - end - - if #result_lines == 0 then - -- Preserve empty lines. - result_lines[1] = "" - end - - return result_lines -end - --- Automatically wraps lines within given array, --- attempting to limit line length to `max_length`. --- Existing line splits are preserved. -local function autowrap(lines, max_length) - local result_lines = {} - - for _, line in ipairs(lines) do - local autowrapped_lines = autowrap_line(line, max_length) - - for _, autowrapped_line in ipairs(autowrapped_lines) do - table.insert(result_lines, autowrapped_line) - end - end - - return result_lines -end - -function Parser:_get_element_help(element) - local label_lines = element:_get_label_lines() - local description_lines = split_lines(element:_get_description()) - - local result_lines = {} - - -- All label lines should have the same length (except the last one, it has no comma). - -- If too long, start description after all the label lines. - -- Otherwise, combine label and description lines. - - local usage_margin_len = self:_inherit_property("help_usage_margin", 3) - local usage_margin = (" "):rep(usage_margin_len) - local description_margin_len = self:_inherit_property("help_description_margin", 25) - local description_margin = (" "):rep(description_margin_len) - - local help_max_width = self:_inherit_property("help_max_width") - - if help_max_width then - local description_max_width = math.max(help_max_width - description_margin_len, 10) - description_lines = autowrap(description_lines, description_max_width) - end - - if #label_lines[1] >= (description_margin_len - usage_margin_len) then - for _, label_line in ipairs(label_lines) do - table.insert(result_lines, usage_margin .. label_line) - end - - for _, description_line in ipairs(description_lines) do - table.insert(result_lines, description_margin .. description_line) - end - else - for i = 1, math.max(#label_lines, #description_lines) do - local label_line = label_lines[i] - local description_line = description_lines[i] - - local line = "" - - if label_line then - line = usage_margin .. label_line - end - - if description_line and description_line ~= "" then - line = line .. (" "):rep(description_margin_len - #line) .. description_line - end - - table.insert(result_lines, line) - end - end - - return table.concat(result_lines, "\n") -end - -local function get_group_types(group) - local types = {} - - for _, element in ipairs(group) do - types[getmetatable(element)] = true - end - - return types -end - -function Parser:_add_group_help(blocks, added_elements, label, elements) - local buf = {label} - - for _, element in ipairs(elements) do - if not element._hidden and not added_elements[element] then - added_elements[element] = true - table.insert(buf, self:_get_element_help(element)) - end - end - - if #buf > 1 then - table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) - end -end - -function Parser:get_help() - if self._help then - return self._help - end - - local blocks = {self:get_usage()} - - local help_max_width = self:_inherit_property("help_max_width") - - if self._description then - local description = self._description - - if help_max_width then - description = table.concat(autowrap(split_lines(description), help_max_width), "\n") - end - - table.insert(blocks, description) - end - - -- 1. Put groups containing arguments first, then other arguments. - -- 2. Put remaining groups containing options, then other options. - -- 3. Put remaining groups containing commands, then other commands. - -- Assume that an element can't be in several groups. - local groups_by_type = { - [Argument] = {}, - [Option] = {}, - [Command] = {} - } - - for _, group in ipairs(self._groups) do - local group_types = get_group_types(group) - - for _, mt in ipairs({Argument, Option, Command}) do - if group_types[mt] then - table.insert(groups_by_type[mt], group) - break - end - end - end - - local default_groups = { - {name = "Arguments", type = Argument, elements = self._arguments}, - {name = "Options", type = Option, elements = self._options}, - {name = "Commands", type = Command, elements = self._commands} - } - - local added_elements = {} - - for _, default_group in ipairs(default_groups) do - local type_groups = groups_by_type[default_group.type] - - for _, group in ipairs(type_groups) do - self:_add_group_help(blocks, added_elements, group.name .. ":", group) - end - - local default_label = default_group.name .. ":" - - if #type_groups > 0 then - default_label = "Other " .. default_label:gsub("^.", string.lower) - end - - self:_add_group_help(blocks, added_elements, default_label, default_group.elements) - end - - if self._epilog then - local epilog = self._epilog - - if help_max_width then - epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") - end - - table.insert(blocks, 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, index) - local converter = self.element._convert - - if converter then - local ok, err - - if type(converter) == "function" then - ok, err = converter(argument) - elseif type(converter[index]) == "function" then - ok, err = converter[index](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:set_name(alias) - self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) -end - -function ElementState:invoke() - self.open = true - self.overwrite = false - - if self.invocations >= self.element._maxcount then - if self.element._overwrite then - self.overwrite = true - else - local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) - self:error("%s must be used %s", self.name, num_times_repr) - 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, #self.args + 1) - 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 - - if self.element._maxargs == 0 then - args = self.args[1] - elseif self.element._maxargs == 1 then - if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then - args = self.args - else - args = self.args[1] - end - else - 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_element_state = {}, - 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, {action = parser._action, name = parser._name}) - 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 _, element in ipairs(mutex) do - if not self.element_to_mutexes[element] then - self.element_to_mutexes[element] = {} - end - - table.insert(self.element_to_mutexes[element], mutex) - end - end - - for _, argument in ipairs(parser._arguments) do - argument = ElementState(self, argument) - table.insert(self.arguments, argument) - argument:set_name() - 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:check_mutexes(element_state) - if self.element_to_mutexes[element_state.element] then - for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do - local used_element_state = self.mutex_to_element_state[mutex] - - if used_element_state and used_element_state ~= element_state then - self:error("%s can not be used together with %s", element_state.name, used_element_state.name) - else - self.mutex_to_element_state[mutex] = element_state - end - end - end -end - -function ParseState:invoke(option, name) - self:close() - option:set_name(name) - self:check_mutexes(option, name) - - if option:invoke() 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 - self:check_mutexes(self.argument) - - 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 - - if self.parser._command_target then - self.result[self.parser._command_target] = command._name - end - - 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 - option.name = option.name or ("option '%s'"):format(option.element._name) - - if option.invocations == 0 then - if option:default("u") then - option:invoke() - 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() - option:close() - end - elseif option.invocations == 0 then - self:error("missing %s", option.name) - else - self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) - end - end - end - - for i = #self.command_actions, 1, -1 do - self.command_actions[i].action(self.result, self.command_actions[i].name) - 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 - if self.options[arg] then - local option = self:get_option(arg) - self:invoke(option, arg) - else - self:close() - end - - 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 - -local argparse = {} - -argparse.version = "0.6.0" - -setmetatable(argparse, {__call = function(_, ...) - return Parser(default_cmdline[0]):add_help(true)(...) -end}) - -return argparse diff -Nru luacheck-0.22.0/src/luacheck/builtin_standards.lua luacheck-0.23.0/src/luacheck/builtin_standards.lua --- luacheck-0.22.0/src/luacheck/builtin_standards.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/builtin_standards.lua 2018-09-18 19:43:27.000000000 +0000 @@ -251,7 +251,7 @@ builtin_standards[name] = def_to_std(def) end -local function detect_default_std() +local function get_running_lua_std_name() if rawget(_G, "jit") then return "luajit" elseif _VERSION == "Lua 5.1" then @@ -265,7 +265,7 @@ end end -builtin_standards._G = builtin_standards[detect_default_std()] +builtin_standards._G = builtin_standards[get_running_lua_std_name()] builtin_standards.busted = { read_globals = { @@ -279,8 +279,18 @@ builtin_standards.rockspec = { globals = { - "rockspec_format", "package", "version", "description", "supported_platforms", - "dependencies", "external_dependencies", "source", "build" + "rockspec_format", "package", "version", "description", "dependencies", "supported_platforms", + "external_dependencies", "source", "build", "hooks", "deploy", "build_dependencies", "test_dependencies", "test" + } +} + +builtin_standards.luacheckrc = { + globals = { + "global", "unused", "redefined", "unused_args", "unused_secondaries", "self", "compat", "allow_defined", + "allow_defined_top", "module", "globals", "read_globals", "new_globals", "new_read_globals", "not_globals", + "ignore", "enable", "only", "std", "max_line_length", "max_code_line_length", "max_string_line_length", + "max_comment_line_length", "max_cyclomatic_complexity", "quiet", "color", "codes", "ranges", "formatter", + "cache", "jobs", "files", "stds", "exclude_files", "include_files" } } diff -Nru luacheck-0.22.0/src/luacheck/cache.lua luacheck-0.23.0/src/luacheck/cache.lua --- luacheck-0.22.0/src/luacheck/cache.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/cache.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,3 +1,5 @@ +local parse_inline_options = require "luacheck.stages.parse_inline_options" +local stages = require "luacheck.stages" local utils = require "luacheck.utils" local cache = {} @@ -7,9 +9,9 @@ -- 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. +-- Event fields are compressed into array indexes. -cache.format_version = 27 +cache.format_version = 34 local option_fields = { "ignore", "std", "globals", "unused_args", "self", "compat", "global", "unused", "redefined", @@ -19,16 +21,7 @@ "max_cyclomatic_complexity" } -local event_fields = { - "code", "name", "line", "column", "end_column", "prev_line", "prev_column", "prev_end_column", "secondary", - "self", "func", "top", "msg", "index", "recursive", "mutually_recursive", "useless", - "field", "label", "push", "pop", "options", "indirect", "indexing", "previous_indexing_len", - "overwritten_line", "overwritten_column", "overwritten_end_column", "complexity", "function_name", "function_type" -} - --- Recursively replace string keys with integer keys. -local function compress(t, fields) - fields = fields or event_fields +local function compress_table(t, fields) local res = {} for index, field in ipairs(fields) do @@ -36,7 +29,7 @@ if value ~= nil then if field == "options" then - value = compress(value, option_fields) + value = compress_table(value, option_fields) end res[index] = value @@ -46,23 +39,27 @@ return res end -local function compress_report(report) +local function compress_tables(tables, per_code_fields) local res = {} - res[1] = utils.map(compress, report.events) - res[2] = {} - for line, events in pairs(report.per_line_options) do - res[2][line] = utils.map(compress, events) + for _, t in ipairs(tables) do + local fields = per_code_fields and stages.warnings[t.code].fields or parse_inline_options.inline_option_fields + table.insert(res, compress_table(t, fields)) end + return res +end + +local function compress_report(report) + local res = {} + res[1] = compress_tables(report.warnings, true) + res[2] = compress_tables(report.inline_options) res[3] = report.line_lengths res[4] = report.line_endings return res end --- Recursively restores a table from a compressed array. -local function decompress(t, fields) - fields = fields or event_fields +local function decompress_table(t, fields) local res = {} for index, field in ipairs(fields) do @@ -70,7 +67,7 @@ if value ~= nil then if field == "options" then - value = decompress(value, option_fields) + value = decompress_table(value, option_fields) end res[field] = value @@ -80,15 +77,28 @@ return res end -local function decompress_report(compressed) - local report = {} - report.events = utils.map(decompress, compressed[1]) - report.per_line_options = {} +local function decompress_tables(tables, per_code_fields) + local res = {} - for line, events in pairs(compressed[2]) do - report.per_line_options[line] = utils.map(decompress, events) + for _, t in ipairs(tables) do + local fields + + if per_code_fields then + fields = stages.warnings[t[1]].fields + else + fields = parse_inline_options.inline_option_fields + end + + table.insert(res, decompress_table(t, fields)) end + return res +end + +local function decompress_report(compressed) + local report = {} + report.warnings = decompress_tables(compressed[1], true) + report.inline_options = decompress_tables(compressed[2]) report.line_lengths = compressed[3] report.line_endings = compressed[4] return report @@ -112,7 +122,7 @@ -- `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 add_value(buffer, strings, value) +local function add_value(buffer, strings, value, level) if type(value) == "string" then local prev = strings[value] @@ -131,25 +141,36 @@ strings[value] = #buffer end elseif type(value) == "table" then + -- Level 1 has the report, level 2 has warning/inline option/line info arrays, + -- level 3 has warnings/inline option containers, level 4 has inline options. + local allow_sparse = level ~= 3 + local nil_tail_start local is_sparse local put_one table.insert(buffer, "{") - for i = 1, max_n(value) do - local item = value[i] + for index = 1, max_n(value) do + local item = value[index] if item == nil then - is_sparse = true + is_sparse = allow_sparse + nil_tail_start = nil_tail_start or index else if put_one then table.insert(buffer, ",") end if is_sparse then - table.insert(buffer, ("[%d]="):format(i)) + table.insert(buffer, ("[%d]="):format(index)) + elseif nil_tail_start then + for _ = nil_tail_start, index - 1 do + table.insert(buffer, "nil,") + end + + nil_tail_start = nil end - add_value(buffer, strings, item) + add_value(buffer, strings, item, level + 1) put_one = true end end @@ -164,7 +185,7 @@ function cache.serialize(report) local strings = {} local buffer = {"", "return "} - add_value(buffer, strings, compress_report(report)) + add_value(buffer, strings, compress_report(report), 1) if strings[1] then local names = {} diff -Nru luacheck-0.22.0/src/luacheck/check.lua luacheck-0.23.0/src/luacheck/check.lua --- luacheck-0.22.0/src/luacheck/check.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/check.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,68 +1,41 @@ -local detect_bad_whitespace = require "luacheck.detect_bad_whitespace" -local detect_cyclomatic_complexity = require "luacheck.detect_cyclomatic_complexity" -local detect_globals = require "luacheck.detect_globals" -local detect_uninit_access = require "luacheck.detect_uninit_access" -local detect_unreachable_code = require "luacheck.detect_unreachable_code" -local detect_unused_locals = require "luacheck.detect_unused_locals" -local detect_unused_rec_funcs = require "luacheck.detect_unused_rec_funcs" -local inline_options = require "luacheck.inline_options" -local linearize = require "luacheck.linearize" -local name_functions = require "luacheck.name_functions" +local check_state = require "luacheck.check_state" +local core_utils = require "luacheck.core_utils" +local parse_inline_options = require "luacheck.stages.parse_inline_options" local parser = require "luacheck.parser" -local resolve_locals = require "luacheck.resolve_locals" +local stages = require "luacheck.stages" local utils = require "luacheck.utils" -local function new_empty_statement_warning(location) - return { - code = "551", - line = location.line, - column = location.column, - end_column = location.column - } -end +local inline_option_fields = utils.array_to_set(parse_inline_options.inline_option_fields) -local function detect_empty_statements(chstate) - for _, location in ipairs(chstate.useless_semicolons) do - table.insert(chstate.warnings, new_empty_statement_warning(location)) +local function validate_fields(tables, per_code_fields) + for _, t in ipairs(tables) do + local fields_set + + if per_code_fields then + if not t.code then + error("Warning has no code", 0) + end + + local warning_info = stages.warnings[t.code] + + if not warning_info then + error("Unknown issue code " .. t.code, 0) + end + + fields_set = warning_info.fields_set + else + fields_set = inline_option_fields + end + + for field in pairs(t) do + if not fields_set[field] then + error("Unknown field " .. field .. " in " .. + (per_code_fields and "issue with code " .. t.code or "inline option table"), 0) + end + end end end -local function check_or_throw(src) - local ast, comments, code_lines, line_endings, useless_semicolons = parser.parse(src) - - local chstate = { - ast = ast, - comments = comments, - code_lines = code_lines, - line_endings = line_endings, - useless_semicolons = useless_semicolons, - source_lines = utils.split_lines(src), - warnings = {} - } - - linearize(chstate) - name_functions(chstate) - resolve_locals(chstate) - - detect_bad_whitespace(chstate) - detect_cyclomatic_complexity(chstate) - detect_empty_statements(chstate) - detect_globals(chstate) - detect_uninit_access(chstate) - detect_unreachable_code(chstate) - detect_unused_locals(chstate) - detect_unused_rec_funcs(chstate) - - local events, per_line_options = inline_options.get_events(chstate) - - return { - events = events, - per_line_options = per_line_options, - line_lengths = utils.map(function(s) return #s end, chstate.source_lines), - line_endings = line_endings - } -end - --- Checks source. -- Returns a table with results, with the following fields: -- `events`: array of issues and inline option events (options, push, or pop). @@ -71,32 +44,53 @@ -- `line_endings`: map from line numbers to "comment", "string", or `nil` base on -- whether the line ending is within a token. -- If `events` array contains a syntax error, the other fields are empty tables. -local function check(src) - local ok, res = utils.try(check_or_throw, src) +local function check(source) + local chstate = check_state.new(source) + local ok, error_wrapper = utils.try(stages.run, chstate) + local warnings, inline_options, line_lengths, line_endings if ok then - return res - elseif utils.is_instance(res.err, parser.SyntaxError) then + warnings = chstate.warnings + core_utils.sort_by_location(warnings) + inline_options = chstate.inline_options + line_lengths = chstate.line_lengths + line_endings = chstate.line_endings + else + local err = error_wrapper.err + + if not utils.is_instance(err, parser.SyntaxError) then + error(error_wrapper, 0) + end + local syntax_error = { code = "011", - line = res.err.line, - column = res.err.column, - end_column = res.err.end_column, - prev_line = res.err.prev_line, - prev_column = res.err.prev_column, - prev_end_column = res.err.prev_end_column, - msg = res.err.msg + line = err.line, + column = chstate:offset_to_column(err.line, err.offset), + end_column = chstate:offset_to_column(err.line, err.end_offset), + msg = err.msg } - return { - events = {syntax_error}, - per_line_options = {}, - line_lengths = {}, - line_endings = {} - } - else - error(res, 0) + if err.prev_line then + syntax_error.prev_line = err.prev_line + syntax_error.prev_column = chstate:offset_to_column(err.prev_line, err.prev_offset) + syntax_error.prev_end_column = chstate:offset_to_column(err.prev_line, err.prev_end_offset) + end + + warnings = {syntax_error} + inline_options = {} + line_lengths = {} + line_endings = {} end + + validate_fields(warnings, true) + validate_fields(inline_options) + + return { + warnings = warnings, + inline_options = inline_options, + line_lengths = line_lengths, + line_endings = line_endings + } end return check diff -Nru luacheck-0.22.0/src/luacheck/check_state.lua luacheck-0.23.0/src/luacheck/check_state.lua --- luacheck-0.22.0/src/luacheck/check_state.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/check_state.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,66 @@ +local utils = require "luacheck.utils" + +local check_state = {} + +local CheckState = utils.class() + +function CheckState:__init(source_bytes) + self.source_bytes = source_bytes + self.warnings = {} +end + +-- Returns column of a character in a line given its offset. +-- The column is never larger than the line length. +-- This can be called if line length is not yet known. +function CheckState:offset_to_column(line, offset) + local line_length = self.line_lengths[line] + local column = offset - self.line_offsets[line] + 1 + + if not line_length then + return column + end + + return math.max(1, math.min(line_length, column)) +end + +function CheckState:warn_column_range(code, range, warning) + warning = warning or {} + warning.code = code + warning.line = range.line + warning.column = range.column + warning.end_column = range.end_column + table.insert(self.warnings, warning) + return warning +end + +function CheckState:warn(code, line, offset, end_offset, warning) + warning = warning or {} + warning.code = code + warning.line = line + warning.column = self:offset_to_column(line, offset) + warning.end_column = self:offset_to_column(line, end_offset) + table.insert(self.warnings, warning) + return warning +end + +function CheckState:warn_range(code, range, warning) + return self:warn(code, range.line, range.offset, range.end_offset, warning) +end + +function CheckState:warn_var(code, var, warning) + warning = self:warn_range(code, var.node, warning) + warning.name = var.name + return warning +end + +function CheckState:warn_value(code, value, warning) + warning = self:warn_range(code, value.var_node, warning) + warning.name = value.var.name + return warning +end + +function check_state.new(source_bytes) + return CheckState(source_bytes) +end + +return check_state diff -Nru luacheck-0.22.0/src/luacheck/config.lua luacheck-0.23.0/src/luacheck/config.lua --- luacheck-0.22.0/src/luacheck/config.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/config.lua 2018-09-18 19:43:27.000000000 +0000 @@ -100,7 +100,7 @@ global_path = global_path or config.get_default_global_path() if global_path and fs.is_file(global_path) then - return global_path + return global_path, (fs.split_base(global_path)) end end @@ -203,6 +203,35 @@ utils.update(env, special_values) end +local function set_default_std(files, pattern, std) + -- Avoid mutating option tables, they may be shared between different patterns. + local pattern_opts = {std = std} + + if files[pattern] then + pattern_opts = utils.update(pattern_opts, files[pattern]) + end + + files[pattern] = pattern_opts +end + +local function add_default_path_options(opts) + local files = {} + + if opts.files then + files = utils.update(files, opts.files) + end + + opts.files = files + set_default_std(files, "**/spec/**/*_spec.lua", "+busted") + set_default_std(files, "**/test/**/*_spec.lua", "+busted") + set_default_std(files, "**/tests/**/*_spec.lua", "+busted") + set_default_std(files, "**/*.rockspec", "+rockspec") + set_default_std(files, "**/*.luacheckrc", "+luacheckrc") +end + +local fallback_config = {options = {}, anchor_dir = ""} +add_default_path_options(fallback_config.options) + -- Loads config from a file, if possible. -- `path` and `global_path` can be nil (will use default), false (will disable loading), or a string. -- Doesn't validate the config. @@ -214,7 +243,7 @@ if anchor_dir then return nil, anchor_dir else - return {options = {}} + return fallback_config end end @@ -234,7 +263,7 @@ end remove_env_mt(env, special_values) - + add_default_path_options(env) return {options = env, config_path = config_path, anchor_dir = anchor_dir} end @@ -465,7 +494,13 @@ local current_dir = fs.get_current_dir() local abs_filename = fs.normalize(fs.join(current_dir, filename)) - local anchor_dir = conf.anchor_dir or current_dir + local anchor_dir + + if conf.anchor_dir == "" then + anchor_dir = fs.split_base(current_dir) + else + anchor_dir = conf.anchor_dir or current_dir + end local matching_pairs = {} diff -Nru luacheck-0.22.0/src/luacheck/core_utils.lua luacheck-0.23.0/src/luacheck/core_utils.lua --- luacheck-0.22.0/src/luacheck/core_utils.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/core_utils.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,73 +1,95 @@ -local core_utils = {} - --- Calls callback with line, index, item, ... for each item reachable from starting item. --- `visited` is a set of already visited indexes. --- Callback can return true to stop walking from current item. -function core_utils.walk_line(line, visited, index, callback, ...) - if visited[index] then - return - end +local decoder = require "luacheck.decoder" +local utils = require "luacheck.utils" - visited[index] = true - - local item = line.items[index] +local core_utils = {} - if callback(line, index, item, ...) then - return - end +-- Attempts to evaluate a node as a Lua value, without resolving locals. +-- Returns Lua value and its string representation on success, nothing on failure. +function core_utils.eval_const_node(node) + if node.tag == "True" then + return true, "true" + elseif node.tag == "False" then + return false, "false" + elseif node.tag == "String" then + local chars = decoder.decode(node[1]) + return node[1], chars:get_printable_substring(1, chars:get_length()) + else + local is_negative - if not item then - return - elseif item.tag == "Jump" then - return core_utils.walk_line(line, visited, item.to, callback, ...) - elseif item.tag == "Cjump" then - core_utils.walk_line(line, visited, item.to, callback, ...) + if node.tag == "Op" and node[1] == "unm" then + is_negative = true + node = node[2] + end + + if node.tag ~= "Number" then + return + end + + local str = node[1] + + if str:find("[iIuUlL]") then + -- Ignore LuaJIT cdata literals. + return + end + + -- On Lua 5.3 convert to float to get same results as on Lua 5.1 and 5.2. + if _VERSION == "Lua 5.3" and not str:find("[%.eEpP]") then + str = str .. ".0" + end + + local number = tonumber(str) + + if not number then + return + end + + if is_negative then + number = -number + end + + if number == number and number < 1/0 and number > -1/0 then + return number, (is_negative and "-" or "") .. node[1] + end end - - return core_utils.walk_line(line, visited, index + 1, 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 statement_containing_tags = utils.array_to_set({"Do", "While", "Repeat", "Fornum", "Forin", "If"}) --- Returns `true` if a variable should be reported as a function instead of simply local, --- `false` otherwise. --- A variable is considered a function if it has a single assignment and the value is a function, --- or if there is a forward declaration with a function assignment later. -function core_utils.is_function_var(var) - return (#var.values == 1 and var.values[1].type == "func") or ( - #var.values == 2 and var.values[1].empty and var.values[2].type == "func") +-- `items` is an array of nodes or nested item arrays. +local function scan_for_statements(chstate, items, tags, callback, ...) + for _, item in ipairs(items) do + if tags[item.tag] then + callback(chstate, item, ...) + end + + if not item.tag or statement_containing_tags[item.tag] then + scan_for_statements(chstate, item, tags, callback, ...) + end + end end -local function event_priority(event) - -- Inline option boundaries have priority over inline option declarations - -- so that `-- luacheck: push ignore foo` is interpreted correctly (push first). - if event.push or event.pop then - return -2 - elseif event.options then - return -1 - else - return tonumber(event.code) +-- Calls `callback(chstate, node, ...)` for each statement node within AST with tag in given array. +function core_utils.each_statement(chstate, tags_array, callback, ...) + local tags = utils.array_to_set(tags_array) + + for _, line in ipairs(chstate.lines) do + scan_for_statements(chstate, line.node[2], tags, callback, ...) end end -local function event_comparator(event1, event2) - if event1.line ~= event2.line then - return event1.line < event2.line - elseif event1.column ~= event2.column then - return event1.column < event2.column +local function location_comparator(warning1, warning2) + if warning1.line ~= warning2.line then + return warning1.line < warning2.line + elseif warning1.column ~= warning2.column then + return warning1.column < warning2.column else - return event_priority(event1) < event_priority(event2) + return warning1.code < warning2.code end end --- Sorts an array of warnings, inline options (tables with `options` field) --- or inline option boundaries (tables with `push` or `pop` field) by location --- information as provided in `line` and `column` fields. -function core_utils.sort_by_location(array) - table.sort(array, event_comparator) +-- Sorts an array of warnings by location information as provided in `line` and `column` fields. +function core_utils.sort_by_location(warnings) + table.sort(warnings, location_comparator) end return core_utils diff -Nru luacheck-0.22.0/src/luacheck/decoder.lua luacheck-0.23.0/src/luacheck/decoder.lua --- luacheck-0.22.0/src/luacheck/decoder.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/decoder.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,199 @@ +local unicode = require "luacheck.unicode" +local utils = require "luacheck.utils" + +local decoder = {} + +local sbyte = string.byte +local sfind = string.find +local sgsub = string.gsub +local ssub = string.sub + +-- `LatinChars` and `UnicodeChars` objects represent source strings +-- and provide Unicode-aware access to them with a common interface. +-- Source bytes should not be accessed directly. +-- Provided methods are: +-- `Chars:get_codepoint(index)`: returns codepoint at given index as integer or nil if index is out of range. +-- `Chars:get_substring(from, to)`: returns substring of original bytes corresponding to characters from `from` to `to`. +-- `Chars:get_printable_substring(from. to)`: like get_substring but escapes not printable characters. +-- `Chars:get_length()`: returns total number of characters. +-- `Chars:find(pattern, from)`: `string.find` but `from` is in characters. Return values are still in bytes. + +-- `LatinChars` is an optimized special case for latin1 strings. +local LatinChars = utils.class() + +function LatinChars:__init(bytes) + self._bytes = bytes +end + +function LatinChars:get_codepoint(index) + return sbyte(self._bytes, index) +end + +function LatinChars:get_substring(from, to) + return ssub(self._bytes, from, to) +end + +local function hexadecimal_escaper(byte) + return ("\\x%02X"):format(sbyte(byte)) +end + +function LatinChars:get_printable_substring(from, to) + return (sgsub(ssub(self._bytes, from, to), "[^\32-\126]", hexadecimal_escaper)) +end + +function LatinChars:get_length() + return #self._bytes +end + +function LatinChars:find(pattern, from) + return sfind(self._bytes, pattern, from) +end + +-- Decodes `bytes` as UTF8. Returns arrays of codepoints as integers and their byte offsets. +-- Byte offsets have one extra item pointing to one byte past the end of `bytes`. +-- On decoding error returns nothing. +local function get_codepoints_and_byte_offsets(bytes) + local codepoints = {} + local byte_offsets = {} + + local byte_index = 1 + local codepoint_index = 1 + + while true do + byte_offsets[codepoint_index] = byte_index + + -- Attempt to decode the next codepoint from UTF8. + local codepoint = sbyte(bytes, byte_index) + + if not codepoint then + return codepoints, byte_offsets + end + + byte_index = byte_index + 1 + + if codepoint >= 0x80 then + -- Not ASCII. + + if codepoint < 0xC0 then + return + end + + local cont = (sbyte(bytes, byte_index) or 0) - 0x80 + + if cont < 0 or cont >= 0x40 then + return + end + + byte_index = byte_index + 1 + + if codepoint < 0xE0 then + -- Two bytes. + codepoint = cont + (codepoint - 0xC0) * 0x40 + elseif codepoint < 0xF0 then + -- Three bytes. + codepoint = cont + (codepoint - 0xE0) * 0x40 + + cont = (sbyte(bytes, byte_index) or 0) - 0x80 + + if cont < 0 or cont >= 0x40 then + return + end + + byte_index = byte_index + 1 + + codepoint = cont + codepoint * 0x40 + elseif codepoint < 0xF8 then + -- Four bytes. + codepoint = cont + (codepoint - 0xF0) * 0x40 + + cont = (sbyte(bytes, byte_index) or 0) - 0x80 + + if cont < 0 or cont >= 0x40 then + return + end + + byte_index = byte_index + 1 + + codepoint = cont + codepoint * 0x40 + + cont = (sbyte(bytes, byte_index) or 0) - 0x80 + + if cont < 0 or cont >= 0x40 then + return + end + + byte_index = byte_index + 1 + + codepoint = cont + codepoint * 0x40 + + if codepoint > 0x10FFFF then + return + end + else + return + end + end + + codepoints[codepoint_index] = codepoint + codepoint_index = codepoint_index + 1 + end +end + +-- `UnicodeChars` is the general case for non-latin1 strings. +-- Assumes UTF8, on decoding error falls back to latin1. +local UnicodeChars = utils.class() + +function UnicodeChars:__init(bytes, codepoints, byte_offsets) + self._bytes = bytes + self._codepoints = codepoints + self._byte_offsets = byte_offsets +end + +function UnicodeChars:get_codepoint(index) + return self._codepoints[index] +end + +function UnicodeChars:get_substring(from, to) + local byte_offsets = self._byte_offsets + return ssub(self._bytes, byte_offsets[from], byte_offsets[to + 1] - 1) +end + +function UnicodeChars:get_printable_substring(from, to) + -- This is only called on syntax error, it's okay to be slow. + local parts = {} + + for index = from, to do + local codepoint = self._codepoints[index] + + if unicode.is_printable(codepoint) then + table.insert(parts, self:get_substring(index, index)) + else + table.insert(parts, (codepoint > 255 and "\\u{%X}" or "\\x%02X"):format(codepoint)) + end + end + + return table.concat(parts) +end + +function UnicodeChars:get_length() + return #self._codepoints +end + +function UnicodeChars:find(pattern, from) + return sfind(self._bytes, pattern, self._byte_offsets[from]) +end + +function decoder.decode(bytes) + -- Only use UnicodeChars if necessary. LatinChars isn't much faster but noticeably more memory efficient. + if sfind(bytes, "[\128-\255]") then + local codepoints, byte_offsets = get_codepoints_and_byte_offsets(bytes) + + if codepoints then + return UnicodeChars(bytes, codepoints, byte_offsets) + end + end + + return LatinChars(bytes) +end + +return decoder diff -Nru luacheck-0.22.0/src/luacheck/detect_bad_whitespace.lua luacheck-0.23.0/src/luacheck/detect_bad_whitespace.lua --- luacheck-0.22.0/src/luacheck/detect_bad_whitespace.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_bad_whitespace.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -local function detect_bad_whitespace(chstate) - for line_number, line in ipairs(chstate.source_lines) do - if line ~= "" then - local from, to = line:find("%s+$") - - if from then - local code - - if from == 1 then - -- Line contains only whitespace (thus never considered "code"). - code = "611" - elseif not chstate.line_endings[line_number] then - -- Trailing whitespace on code line or after long comment. - code = "612" - elseif chstate.line_endings[line_number] == "string" then - -- Trailing whitespace embedded in a string literal. - code = "613" - elseif chstate.line_endings[line_number] == "comment" then - -- Trailing whitespace at the end of a line comment or inside long comment. - code = "614" - end - - table.insert(chstate.warnings, {code = code, line = line_number, column = from, end_column = to}) - end - - from, to = line:find("^%s+") - - if from and to ~= #line and line:sub(1, to):find(" \t") then - -- Inconsistent leading whitespace (SPACE followed by TAB). - table.insert(chstate.warnings, {code = "621", line = line_number, column = from, end_column = to}) - end - end - end -end - -return detect_bad_whitespace diff -Nru luacheck-0.22.0/src/luacheck/detect_cyclomatic_complexity.lua luacheck-0.23.0/src/luacheck/detect_cyclomatic_complexity.lua --- luacheck-0.22.0/src/luacheck/detect_cyclomatic_complexity.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_cyclomatic_complexity.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,143 +0,0 @@ -local utils = require "luacheck.utils" - -local function new_cyclomatic_complexity_warning(node, complexity) - local warning = { - code = "561", - complexity = complexity - } - - if node.location then - warning.line = node.location.line - warning.column = node.location.column - warning.end_column = node.location.column + #"function" - 1 - warning.function_name = node.name - warning.function_type = node[1][1] and node[1][1].implicit and "method" or "function" - else - warning.line = 1 - warning.column = 1 - warning.end_column = 1 - warning.function_type = "main_chunk" - end - - return warning -end - -local CyclomaticComplexityMetric = utils.class() - -function CyclomaticComplexityMetric:incr_decisions(count) - self.count = self.count + count -end - -function CyclomaticComplexityMetric:calc_expr(node) - if node.tag == "Op" and (node[1] == "and" or node[1] == "or") then - self:incr_decisions(1) - end - - if node.tag ~= "Function" then - self:calc_exprs(node) - end -end - -function CyclomaticComplexityMetric:calc_exprs(exprs) - for _, expr in ipairs(exprs) do - if type(expr) == "table" then - self:calc_expr(expr) - end - end -end - -function CyclomaticComplexityMetric:calc_item_Eval(item) - self:calc_expr(item.expr) -end - -function CyclomaticComplexityMetric:calc_item_Local(item) - if item.rhs then - self:calc_exprs(item.rhs) - end -end - -function CyclomaticComplexityMetric:calc_item_Set(item) - self:calc_exprs(item.rhs) -end - -function CyclomaticComplexityMetric:calc_item(item) - local f = self["calc_item_" .. item.tag] - if f then - f(self, item) - end -end - -function CyclomaticComplexityMetric:calc_items(items) - for _, item in ipairs(items) do - self:calc_item(item) - end -end - --- stmt if: {condition, block; condition, block; ... else_block} -function CyclomaticComplexityMetric:calc_stmt_If(node) - for i = 1, #node - 1, 2 do - self:incr_decisions(1) - self:calc_stmts(node[i+1]) - end - - if #node % 2 == 1 then - self:calc_stmts(node[#node]) - end -end - --- stmt while: {condition, block} -function CyclomaticComplexityMetric:calc_stmt_While(node) - self:incr_decisions(1) - self:calc_stmts(node[2]) -end - --- stmt repeat: {block, condition} -function CyclomaticComplexityMetric:calc_stmt_Repeat(node) - self:incr_decisions(1) - self:calc_stmts(node[1]) -end - --- stmt forin: {iter_vars, expression_list, block} -function CyclomaticComplexityMetric:calc_stmt_Forin(node) - self:incr_decisions(1) - self:calc_stmts(node[3]) -end - --- stmt fornum: {first_var, expression, expression, expression[optional], block} -function CyclomaticComplexityMetric:calc_stmt_Fornum(node) - self:incr_decisions(1) - self:calc_stmts(node[5] or node[4]) -end - -function CyclomaticComplexityMetric:calc_stmt(node) - local f = self["calc_stmt_" .. node.tag] - if f then - f(self, node) - end -end - -function CyclomaticComplexityMetric:calc_stmts(stmts) - for _, stmt in ipairs(stmts) do - self:calc_stmt(stmt) - end -end - --- Cyclomatic complexity of a function equals to the number of decision points plus 1. -function CyclomaticComplexityMetric:report(chstate, line) - self.count = 1 - self:calc_stmts(line.node[2]) - self:calc_items(line.items) - table.insert(chstate.warnings, new_cyclomatic_complexity_warning(line.node, self.count)) - return self.count + 1 -end - -local function detect_cyclomatic_complexity(chstate) - local ccmetric = CyclomaticComplexityMetric() - ccmetric:report(chstate, chstate.main_line) - - for _, nested_line in ipairs(chstate.main_line.lines) do - ccmetric:report(chstate, nested_line) - end -end - -return detect_cyclomatic_complexity diff -Nru luacheck-0.22.0/src/luacheck/detect_globals.lua luacheck-0.23.0/src/luacheck/detect_globals.lua --- luacheck-0.22.0/src/luacheck/detect_globals.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_globals.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,216 +0,0 @@ -local utils = require "luacheck.utils" - -local action_codes = { - set = "1", - mutate = "2", - access = "3" -} - --- `index` describes an indexing, where `index[1]` is a global node --- and other items describe keys: each one is a string node, "not_string", --- or "unknown". `node` is literal base node that's indexed. --- E.g. in `local a = table.a; a.b = "c"` `node` is `a` node of the second --- statement and `index` describes `table.a.b`. --- `index.previous_indexing_len` is optional length of prefix of `index` array representing last assignment --- in the aliasing chain, e.g. `2` in the previous example (because last indexing is `table.a`). -local function new_global_warning(node, index, is_lhs, is_top_scope) - local global = index[1] - local action = is_lhs and (#index == 1 and "set" or "mutate") or "access" - - local indexing = {} - - for i, field in ipairs(index) do - if field == "unknown" then - indexing[i] = true - elseif field == "not_string" then - indexing[i] = false - else - indexing[i] = field[1] - end - end - - return { - code = "11" .. action_codes[action], - name = global[1], - indexing = indexing, - previous_indexing_len = index.previous_indexing_len, - line = node.location.line, - column = node.location.column, - end_column = node.location.column + #node[1] - 1, - top = is_top_scope and (action == "set") or nil, - indirect = node ~= global or nil - } -end - -local function resolved_to_index(resolution) - return resolution ~= "unknown" and resolution ~= "not_string" and resolution.tag ~= "String" -end - -local literal_tags = utils.array_to_set({"Nil", "True", "False", "Number", "String", "Table", "Function"}) - -local deep_resolve -- Forward declaration. - -local function resolve_node(node, item) - if node.tag == "Id" or node.tag == "Index" then - deep_resolve(node, item) - return node.resolution - elseif literal_tags[node.tag] then - return node.tag == "String" and node or "not_string" - else - return "unknown" - end -end - --- Resolves value of an identifier or index node, tracking through simple --- assignments like `local foo = bar.baz`. --- Can be given an `Invoke` node to resolve the method field. --- Sets `node.resolution` to "unknown", "not_string", `string node`, or --- {previous_indexing_len = index, global_node, key...}. --- Each key can be "unknown", "not_string" or `string_node`. -function deep_resolve(node, item) - if node.resolution then - return - end - - -- Common case. - -- Also protects against infinite recursion, if it's even possible. - node.resolution = "unknown" - - local base = node - local base_tag = node.tag == "Id" and "Id" or "Index" - local keys = {} - - while base_tag == "Index" do - table.insert(keys, 1, base[2]) - base = base[1] - base_tag = base.tag - end - - if base_tag ~= "Id" then - return - end - - local var = base.var - local base_resolution - local previous_indexing_len - - if var then - if not item.used_values[var] or #item.used_values[var] ~= 1 then - -- Do not know where the value for the base local came from. - return - end - - local value = item.used_values[var][1] - - if not value.node then - return - end - - base_resolution = resolve_node(value.node, value.item) - - if resolved_to_index(base_resolution) then - previous_indexing_len = #base_resolution - end - else - base_resolution = {base} - end - - if #keys == 0 then - node.resolution = base_resolution - elseif not resolved_to_index(base_resolution) then - -- Indexing something unknown or indexing a literal. - node.resolution = "unknown" - else - local resolution = utils.update({}, base_resolution) - resolution.previous_indexing_len = previous_indexing_len - - for _, key in ipairs(keys) do - local key_resolution = resolve_node(key, item) - - if resolved_to_index(key_resolution) then - key_resolution = "unknown" - end - - table.insert(resolution, key_resolution) - end - - -- Assign resolution only after all the recursive calls. - node.resolution = resolution - end -end - -local function detect_in_node(chstate, item, node, is_top_line, is_lhs) - if node.tag == "Index" or node.tag == "Invoke" or node.tag == "Id" then - if node.tag == "Id" and node.var then - -- Do not warn about assignments to and accesses of local variables - -- that resolve to globals or their fields. - return - end - - deep_resolve(node, item) - local resolution = node.resolution - - -- Still need to recurse into base and key nodes. - -- E.g. don't miss a global in `(global1())[global2()]. - - if node.tag == "Invoke" then - for i = 3, #node do - detect_in_node(chstate, item, node[i], is_top_line) - end - end - - if node.tag ~= "Id" then - repeat - detect_in_node(chstate, item, node[2], is_top_line) - node = node[1] - until node.tag ~= "Index" - - if node.tag ~= "Id" then - detect_in_node(chstate, item, node, is_top_line) - end - end - - if resolved_to_index(resolution) then - table.insert(chstate.warnings, new_global_warning(node, resolution, is_lhs, is_top_line)) - end - elseif node.tag ~= "Function" then - for _, nested_node in ipairs(node) do - if type(nested_node) == "table" then - detect_in_node(chstate, item, nested_node, is_top_line) - end - end - end -end - -local function detect_in_nodes(chstate, item, nodes, is_top_line, is_lhs) - for _, node in ipairs(nodes) do - detect_in_node(chstate, item, node, is_top_line, is_lhs) - end -end - -local function detect_in_line(chstate, line, is_top_line) - for _, item in ipairs(line.items) do - if item.tag == "Eval" then - detect_in_node(chstate, item, item.expr, is_top_line) - elseif item.tag == "Local" then - if item.rhs then - detect_in_nodes(chstate, item, item.rhs, is_top_line) - end - elseif item.tag == "Set" then - detect_in_nodes(chstate, item, item.lhs, is_top_line, true) - detect_in_nodes(chstate, item, item.rhs, is_top_line) - end - end -end - --- Adds warnings for assignments, field accesses, and mutations of global variables, --- tracing through localizing assignments such as `local t = table`. -local function detect_globals(chstate) - detect_in_line(chstate, chstate.main_line, true) - - for _, nested_line in ipairs(chstate.main_line.lines) do - detect_in_line(chstate, nested_line) - end -end - -return detect_globals diff -Nru luacheck-0.22.0/src/luacheck/detect_uninit_access.lua luacheck-0.23.0/src/luacheck/detect_uninit_access.lua --- luacheck-0.22.0/src/luacheck/detect_uninit_access.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_uninit_access.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -local function new_uninit_warning(node, is_mutation) - return { - code = is_mutation and "341" or "321", - name = node[1], - line = node.location.line, - column = node.location.column, - end_column = node.location.column + #node[1] - 1 - } -end - -local function detect_uninit_access_in_line(chstate, line) - for _, item in ipairs(line.items) do - for _, action_key in ipairs({"accesses", "mutations"}) do - local item_var_map = item[action_key] - - if item_var_map then - for var, accessing_nodes in pairs(item_var_map) do - -- If there are no values at all reaching this access, not even the empty one, - -- this item (or a closure containing it) is not reachable from variable definition. - -- It will be reported as unreachable code, no need to report uninitalized accesses in it. - if item.used_values[var] then - -- If this variable is has only one, empty value then it's already reported as never set, - -- no need to report each access. - if not (#var.values == 1 and var.values[1].empty) then - local all_possible_values_empty = true - - for _, possible_value in ipairs(item.used_values[var]) do - if not possible_value.empty then - all_possible_values_empty = false - break - end - end - - if all_possible_values_empty then - for _, accessing_node in ipairs(accessing_nodes) do - table.insert(chstate.warnings, new_uninit_warning(accessing_node, action_key == "mutations")) - end - end - end - end - end - end - end - end -end - --- Adds warnings for accesses that don't resolve to any values except initial empty one. -local function detect_uninit_access(chstate) - detect_uninit_access_in_line(chstate, chstate.main_line) - - for _, nested_line in ipairs(chstate.main_line.lines) do - detect_uninit_access_in_line(chstate, nested_line) - end -end - -return detect_uninit_access diff -Nru luacheck-0.22.0/src/luacheck/detect_unreachable_code.lua luacheck-0.23.0/src/luacheck/detect_unreachable_code.lua --- luacheck-0.22.0/src/luacheck/detect_unreachable_code.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_unreachable_code.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -local core_utils = require "luacheck.core_utils" - -local function new_unreachable_code_warning(location, is_unrepeatable_loop, token) - return { - code = "51" .. (is_unrepeatable_loop and "2" or "1"), - line = location.line, - column = location.column, - end_column = location.column + #token - 1 - } -end - -local function noop_callback() end - -local function detect_unreachable_code_in_line(chstate, line) - local reachable_indexes = {} - - -- Mark all items reachable from the function start. - core_utils.walk_line(line, reachable_indexes, 1, noop_callback) - - -- All remaining items are unreachable. - -- However, there is no point in reporting all of them. - -- Only report those that are not reachable from any already reported ones. - for i, item in ipairs(line.items) do - if not reachable_indexes[i] then - if item.location then - table.insert(chstate.warnings, new_unreachable_code_warning(item.location, item.loop_end, item.token)) - -- Mark all items reachable from the item just reported. - core_utils.walk_line(line, reachable_indexes, i, noop_callback) - end - end - end -end - --- Adds warnings for unreachable code. -local function detect_unreachable_code(chstate) - detect_unreachable_code_in_line(chstate, chstate.main_line) - - for _, nested_line in ipairs(chstate.main_line.lines) do - detect_unreachable_code_in_line(chstate, nested_line) - end -end - -return detect_unreachable_code diff -Nru luacheck-0.22.0/src/luacheck/detect_unused_locals.lua luacheck-0.23.0/src/luacheck/detect_unused_locals.lua --- luacheck-0.22.0/src/luacheck/detect_unused_locals.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_unused_locals.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,185 +0,0 @@ -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 type_codes = { - var = "1", - func = "1", - arg = "2", - loop = "3", - loopi = "3" -} - -local function new_unused_var_warning(value, is_useless) - return { - code = "21" .. type_codes[value.var.type], - name = value.var.name, - line = value.location.line, - column = value.location.column, - end_column = value.location.column + (value.var.self and #":" or #value.var.name) - 1, - secondary = is_secondary(value) or nil, - func = (value.type == "func") or nil, - self = value.var.self, - useless = value.var.name == "_" and is_useless or nil - } -end - -local function new_unset_var_warning(var) - return { - code = "221", - name = var.name, - line = var.location.line, - column = var.location.column, - end_column = var.location.column + #var.name - 1 - } -end - -local function new_unaccessed_var_warning(var, was_mutated) - -- 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 - - return { - code = "2" .. (was_mutated and "4" or "3") .. type_codes[var.type], - name = var.name, - line = var.location.line, - column = var.location.column, - end_column = var.location.column + (var.self and #":" or #var.name) - 1, - secondary = secondary - } -end - -local function new_unused_value_warning(value, was_mutated, overwriting_node) - return { - code = "3" .. (was_mutated and "3" or "1") .. type_codes[value.type], - name = value.var.name, - overwritten_line = overwriting_node and overwriting_node.location.line, - overwritten_column = overwriting_node and overwriting_node.location.column, - overwritten_end_column = overwriting_node and (overwriting_node.location.column + #value.var.name - 1), - line = value.location.line, - column = value.location.column, - end_column = value.location.column + (value.type == "arg" and value.var.self and #":" or #value.var.name) - 1, - secondary = is_secondary(value) or nil - } -end - -local externally_accessible_tags = utils.array_to_set({"Id", "Index", "Call", "Invoke", "Op", "Paren", "Dots"}) - -local function externally_accessible(value) - return value.type ~= "var" or (value.node and externally_accessible_tags[value.node.tag]) -end - -local function get_overwriting_lhs_node(item, value) - for _, node in ipairs(item.lhs) do - if node.var == value.var then - return node - end - end -end - -local function get_second_overwriting_lhs_node(item, value) - local after_value_node - - for _, node in ipairs(item.lhs) do - if node.var == value.var then - if after_value_node then - return node - elseif node.location == value.location then - after_value_node = true - end - end - end -end - -local function check_var(chstate, var) - if core_utils.is_function_var(var) then - local value = var.values[2] or var.values[1] - - if not value.used then - table.insert(chstate.warnings, new_unused_var_warning(value)) - end - elseif #var.values == 1 then - if not var.values[1].used then - if var.values[1].mutated then - if not externally_accessible(var.values[1]) then - table.insert(chstate.warnings, new_unaccessed_var_warning(var, true)) - end - else - table.insert(chstate.warnings, new_unused_var_warning(var.values[1], var.values[1].empty)) - end - elseif var.values[1].empty then - table.insert(chstate.warnings, new_unset_var_warning(var)) - end - elseif not var.accessed and not var.mutated then - table.insert(chstate.warnings, new_unaccessed_var_warning(var)) - else - local no_values_externally_accessible = true - - for _, value in ipairs(var.values) do - if externally_accessible(value) then - no_values_externally_accessible = false - end - end - - if not var.accessed and no_values_externally_accessible then - table.insert(chstate.warnings, new_unaccessed_var_warning(var, true)) - end - - for _, value in ipairs(var.values) do - if not value.empty then - if not value.used and not value.mutated then - local overwriting_node - - if value.overwriting_item then - if value.overwriting_item ~= value.item then - overwriting_node = get_overwriting_lhs_node(value.overwriting_item, value) - end - else - overwriting_node = get_second_overwriting_lhs_node(value.item, value) - end - - table.insert(chstate.warnings, new_unused_value_warning(value, false, overwriting_node)) - elseif not value.used and not externally_accessible(value) then - if var.accessed or not no_values_externally_accessible then - table.insert(chstate.warnings, new_unused_value_warning(value, true)) - end - end - end - end - end -end - -local function detect_unused_locals_in_line(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 the implicit top level vararg. - if var.location then - check_var(chstate, var) - end - end - end - end -end - --- Detects unused local variables and their values as well as locals that --- are accessed but never set or set but never accessed. -local function detect_unused_locals(chstate) - detect_unused_locals_in_line(chstate, chstate.main_line) - - for _, nested_line in ipairs(chstate.main_line.lines) do - detect_unused_locals_in_line(chstate, nested_line) - end -end - -return detect_unused_locals diff -Nru luacheck-0.22.0/src/luacheck/detect_unused_rec_funcs.lua luacheck-0.23.0/src/luacheck/detect_unused_rec_funcs.lua --- luacheck-0.22.0/src/luacheck/detect_unused_rec_funcs.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/detect_unused_rec_funcs.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,120 +0,0 @@ -local core_utils = require "luacheck.core_utils" - -local function new_unused_rec_func_var_warning(value, is_self_recursive) - return { - code = "211", - name = value.var.name, - line = value.location.line, - column = value.location.column, - end_column = value.location.column + #value.var.name - 1, - func = true, - mutually_recursive = not is_self_recursive or nil, - recursive = is_self_recursive or nil - } -end - -local function new_unused_rec_func_value_warning(value) - return { - code = "311", - name = value.var.name, - line = value.location.line, - column = value.location.column, - end_column = value.location.column + #value.var.name - 1 - } -end - -local function mark_reachable_lines(edges, marked, line) - for connected_line in pairs(edges[line]) do - if not marked[connected_line] then - marked[connected_line] = true - mark_reachable_lines(edges, marked, connected_line) - end - end -end - --- Detects and reports unused recursive and mutually recursive functions. -local function detect_unused_rec_funcs(chstate) - -- Build a graph of usage relations of all closures. - -- Closure A is used by closure B iff either B is parent - -- of A and A is not assigned to a local/upvalue, or - -- B uses local/upvalue value that is A. - -- Closures not reachable from root closure are unused, - -- report corresponding values/variables if not done already. - - local line = chstate.main_line - - -- Initialize edges maps. - local forward_edges = {[line] = {}} - local backward_edges = {[line] = {}} - - for _, nested_line in ipairs(line.lines) do - forward_edges[nested_line] = {} - backward_edges[nested_line] = {} - end - - -- Add edges leading to each nested line. - for _, nested_line in ipairs(line.lines) do - if nested_line.node.value then - for using_line in pairs(nested_line.node.value.using_lines) do - forward_edges[using_line][nested_line] = true - backward_edges[nested_line][using_line] = true - end - elseif nested_line.parent then - forward_edges[nested_line.parent][nested_line] = true - backward_edges[nested_line][nested_line.parent] = true - end - end - - -- Recursively mark all closures reachable from root closure and unused closures. - -- Closures reachable from main chunk are used; closure reachable from unused closures - -- depend on that closure; that is, fixing warning about parent unused closure - -- fixes warning about the child one, so issuing a warning for the child is superfluous. - local marked = {[line] = true} - mark_reachable_lines(forward_edges, marked, line) - - for _, nested_line in ipairs(line.lines) do - if nested_line.node.value and not nested_line.node.value.used then - marked[nested_line] = true - mark_reachable_lines(forward_edges, marked, nested_line) - end - end - - -- Deal with unused closures. - for _, nested_line in ipairs(line.lines) do - local value = nested_line.node.value - - if value and value.used and not marked[nested_line] then - -- This closure is used by some closure, but is not marked as reachable - -- from main chunk or any of reported closures. - -- Find candidate group of mutually recursive functions containing this one: - -- mark sets of closures reachable from it by forward and backward edges, - -- intersect them. Ignore already marked closures in the process to avoid - -- issuing superfluous, dependent warnings. - local forward_marked = setmetatable({}, {__index = marked}) - local backward_marked = setmetatable({}, {__index = marked}) - mark_reachable_lines(forward_edges, forward_marked, nested_line) - mark_reachable_lines(backward_edges, backward_marked, nested_line) - - -- Iterate over closures in the group. - for mut_rec_line in pairs(forward_marked) do - if rawget(backward_marked, mut_rec_line) then - marked[mut_rec_line] = true - value = mut_rec_line.node.value - - if value then - -- Report this closure as self recursive or mutually recursive. - local is_self_recursive = forward_edges[mut_rec_line][mut_rec_line] - - if core_utils.is_function_var(value.var) then - table.insert(chstate.warnings, new_unused_rec_func_var_warning(value, is_self_recursive)) - else - table.insert(chstate.warnings, new_unused_rec_func_value_warning(value)) - end - end - end - end - end - end -end - -return detect_unused_rec_funcs diff -Nru luacheck-0.22.0/src/luacheck/expand_rockspec.lua luacheck-0.23.0/src/luacheck/expand_rockspec.lua --- luacheck-0.22.0/src/luacheck/expand_rockspec.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/expand_rockspec.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,14 +1,75 @@ +local fs = require "luacheck.fs" local utils = require "luacheck.utils" -local function extract_lua_files(rockspec) - if type(rockspec) ~= "table" then - return nil, "rockspec is not a table" +local blacklist = utils.array_to_set({"spec", ".luarocks", "lua_modules", "test.lua", "tests.lua"}) + +-- This reimplements relevant parts of `luarocks.build.builtin.autodetect_modules`. +-- Autodetection works relatively to the directory containing the rockspec. +local function autodetect_modules(rockspec_path) + rockspec_path = fs.normalize(rockspec_path) + local base, rest = fs.split_base(rockspec_path) + local project_dir = base .. (rest:match("^(.*)" .. utils.dir_sep .. ".*$") or "") + + if project_dir == "" then + project_dir = "." + end + + local module_dir = project_dir + + for _, module_subdir in ipairs({"src", "lua", "lib"}) do + local full_module_dir = fs.join(project_dir, module_subdir) + + if fs.is_dir(full_module_dir) then + module_dir = full_module_dir + break + end + end + + local res = {} + + for _, file in ipairs((fs.extract_files(module_dir, "%.lua$"))) do + -- Extract first part of the path from module_dir to the file, or file name itself. + if not blacklist[file:match("^" .. module_dir:gsub("%p", "%%%0") .. "[\\/]*([^\\/]*)")] then + table.insert(res, file) + end + end + + local bin_dir + + for _, bin_subdir in ipairs({"src/bin", "bin"}) do + local full_bin_dir = fs.join(project_dir, bin_subdir) + + if fs.is_dir(full_bin_dir) then + bin_dir = full_bin_dir + end end + if bin_dir then + local iter, state, var = fs.dir_iter(bin_dir) + + if iter then + for basename in iter, state, var do + if basename:sub(-#".lua") == ".lua" then + table.insert(res, fs.join(bin_dir, basename)) + end + end + end + end + + return res +end + +local function extract_lua_files(rockspec_path, rockspec) local build = rockspec.build if type(build) ~= "table" then - return nil, "rockspec.build is not a table" + return autodetect_modules(rockspec_path) + end + + if not build.type or build.type == "builtin" or build.type == "module" then + if not build.modules then + return autodetect_modules(rockspec_path) + end end local res = {} @@ -23,9 +84,7 @@ end end - if build.type == "builtin" then - scan(build.modules) - end + scan(build.modules) if type(build.install) == "table" then scan(build.install.lua) @@ -36,21 +95,16 @@ return res end --- Receives a name of a rockspec, returns list of related .lua files or nil and "syntax" or "error" and error message. -local function expand_rockspec(file) - local rockspec, err, msg = utils.load_config(file) +-- Receives a name of a rockspec, returns list of related .lua files. +-- On error returns nil and "I/O", "syntax", or "runtime" and error message. +local function expand_rockspec(rockspec_path) + local rockspec, err_type, err_msg = utils.load_config(rockspec_path) if not rockspec then - return nil, err, msg - end - - local files, format_err = extract_lua_files(rockspec) - - if not files then - return nil, "syntax", format_err + return nil, err_type, err_msg end - return files + return extract_lua_files(rockspec_path, rockspec) end return expand_rockspec diff -Nru luacheck-0.22.0/src/luacheck/filter.lua luacheck-0.23.0/src/luacheck/filter.lua --- luacheck-0.22.0/src/luacheck/filter.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/filter.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,148 +1,32 @@ -local inline_options = require "luacheck.inline_options" -local options = require "luacheck.options" local core_utils = require "luacheck.core_utils" +local decoder = require "luacheck.decoder" +local options = require "luacheck.options" local utils = require "luacheck.utils" local filter = {} --- 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) - local defined, globally_defined, used = {}, {}, {} - - for _, pair in ipairs(file_report) do - local warning, opts = pair[1], pair[2] - - if warning.code:match("11.") then - if warning.code == "111" then - if core_utils.is_definition(opts, warning) then - if 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) - 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) - 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, globally_defined, globally_used, locally_defined) - local res = {} - - for _, pair in ipairs(file_report) do - local warning, opts = pair[1], pair[2] - - if warning.code:match("11.") then - if warning.code == "111" then - if opts.module then - if not locally_defined[warning.name] then - warning.module = true - table.insert(res, {warning, opts}) - end - else - if core_utils.is_definition(opts, warning) then - if not globally_used[warning.name] then - warning.code = "131" - warning.top = nil - table.insert(res, {warning, opts}) - end - else - if not globally_defined[warning.name] then - table.insert(res, {warning, opts}) - end - end - end - else - if not globally_defined[warning.name] and not locally_defined[warning.name] then - table.insert(res, {warning, opts}) - end - end - else - table.insert(res, {warning, opts}) - end - end - - return res -end - --- Returns report clear of implicit definitions. -local function filter_implicit_defs(report) - local res = {} - local info = get_implicit_defs_info(report) - - for i, file_report in ipairs(report) do - if not file_report.fatal then - res[i] = filter_implicit_defs_file(file_report, 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 function match(pattern, code, name) 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) + matches_code = utils.pmatch(code, code_pattern) end if name_pattern then - if not warning.name then + if not name then -- Warnings without name field can't match by name. matches_name = false else - matches_name = utils.pmatch(warning.name, name_pattern) + matches_name = utils.pmatch(name, name_pattern) end end return matches_code, matches_name end -local function is_enabled(rules, warning) +local function passes_rules_filter(rules, code, name) -- A warning is enabled when its code and name are enabled. local enabled_code, enabled_name = false, false @@ -150,7 +34,7 @@ local matches_one = false for _, pattern in ipairs(rule[1]) do - local matches_code, matches_name = match(warning, pattern) + local matches_code, matches_name = match(pattern, code, name) -- If a factor is enabled, warning can't be disabled by it. if enabled_code then @@ -200,9 +84,19 @@ local function get_field_string(warning) local parts = {} - for i = 2, #warning.indexing do - local index_string = warning.indexing[i] - table.insert(parts, type(index_string) == "string" and index_string or "?") + if warning.indexing then + for _, index in ipairs(warning.indexing) do + local part + + if type(index) == "string" then + local chars = decoder.decode(index) + part = chars:get_printable_substring(1, chars:get_length()) + else + part = "?" + end + + table.insert(parts, part) + end end return table.concat(parts, ".") @@ -213,8 +107,8 @@ local defined = true local read_only = true - for i = 1, depth or #warning.indexing do - local index_string = warning.indexing[i] + for i = 1, depth or (warning.indexing and #warning.indexing or 0) + 1 do + local index_string = i == 1 and warning.name or warning.indexing[i - 1] if index_string == true then -- Indexing with something that may or may not be a string. @@ -259,212 +153,383 @@ return defined and (read_only and "read_only" or "global") or "undefined" end -local function get_max_line_length(opts, warning) - return opts["max_" .. (warning.line_ending or "code") .. "_line_length"] -end +-- Checks if a warning passes options filter. May add some fields required for formatting. +local function passes_filter(normalized_options, warning) + if warning.code == "561" then + local max_complexity = normalized_options.max_cyclomatic_complexity -local function get_max_cyclomatic_complexity(opts) - return opts["max_cyclomatic_complexity"] -end + if not max_complexity or warning.complexity <= max_complexity then + return false + end -local function filters(opts, warning) - if warning.code == "631" then - local max_line_length = get_max_line_length(opts, warning) + warning.max_complexity = max_complexity + elseif warning.code:find("^[234]") and warning.name == "_" and not warning.useless then + return false + elseif warning.code:find("^1[14]") then + if warning.indirect and + get_field_status(normalized_options, warning, warning.previous_indexing_len) == "undefined" then + return false + end - if (not max_line_length or warning.end_column <= max_line_length) then - return true + if not warning.module and get_field_status(normalized_options, warning) ~= "undefined" then + return false end end - if warning.code == "561" then - local max_cyclomatic_complexity = get_max_cyclomatic_complexity(opts, warning) - if (not max_cyclomatic_complexity or warning.complexity <= max_cyclomatic_complexity) then - return true - end + if warning.code:find("^1[24][23]") then + warning.field = get_field_string(warning) end - if warning.code:match("[234]..") and warning.name == "_" and not warning.useless then - return true + if warning.secondary and not normalized_options.unused_secondaries then + return false end - if warning.code:match("1[14].") and warning.indirect and get_field_status( - opts, warning, warning.previous_indexing_len) == "undefined" then - return true + if warning.self and not normalized_options.self then + return false end - if warning.code:match("1[14].") and not warning.module and get_field_status(opts, warning) ~= "undefined" then - return true + return passes_rules_filter(normalized_options.rules, warning.code, warning.name) +end + +local empty_options = {} + +-- Updates option_stack for given line with next_index pointing to the inline option past the previous line. +-- Adds warnings for invalid inline options to check_result, filtered_warnings. +-- Returns updated next_index. +local function update_option_stack_for_new_line(check_result, stds, option_stack, line, next_index) + local inline_option = check_result.inline_options[next_index] + + if not inline_option or inline_option.line > line then + -- No inline options on this line, option stack for the line is ready. + return next_index end - if warning.secondary and not opts.unused_secondaries then - return true + next_index = next_index + 1 + + if inline_option.pop_count then + for _ = 1, inline_option.pop_count do + table.remove(option_stack) + end end - if warning.self and not opts.self then - return true + if not inline_option.options then + -- No inline option push on this line, option stack for the line is ready. + return next_index end - return not is_enabled(opts.rules, warning) -end + local options_ok, err_msg = options.validate(options.all_options, inline_option.options, stds) -local function filter_file_report(report) - local res = {} + if not options_ok then + -- Warn about invalid inline option, push a dummy empty table instead to keep pop counts correct. + inline_option.options = nil + inline_option.code = "021" + inline_option.msg = err_msg + table.insert(check_result.filtered_warnings, inline_option) - for _, pair in ipairs(report) do - local issue, opts = pair[1], pair[2] + -- Reuse empty table identity so that normalized option caching works better. + table.insert(option_stack, empty_options) + else + table.insert(option_stack, inline_option.options) + end - if issue.code:match("11[12]") and not issue.module and get_field_status(opts, issue) == "read_only" then - issue.code = "12" .. issue.code:sub(3, 3) - end + return next_index +end - if issue.code:match("11[23]") and get_field_status(opts, issue, 1) ~= "undefined" then - issue.code = "14" .. issue.code:sub(3, 3) +-- Warns (adds to check_result.filtered_warnings) about a line if it's too long +-- and the warning is not filtered out by options. +local function check_line_length(check_result, normalized_options, line) + local line_length = check_result.line_lengths[line] + local line_type = check_result.line_endings[line] + local max_length = normalized_options["max_" .. (line_type or "code") .. "_line_length"] + + if max_length and line_length > max_length then + if passes_rules_filter(normalized_options.rules, "631") then + table.insert(check_result.filtered_warnings, { + code = "631", + line = line, + column = max_length + 1, + end_column = line_length, + max_length = max_length, + line_ending = line_type + }) end + end +end - if issue.code:match("0..") then - if issue.code == "011" or opts.inline then - table.insert(res, issue) - end - else - if not filters(opts, issue) then - if issue.code == "631" then - issue.max_length = get_max_line_length(opts, issue) - issue.column = issue.max_length + 1 - end +-- Adds warnings passing filtering and not related to globals to check_result.filtered_warnings. +-- If there is a global related warning on this line, sets check_results[line] to normalized_optuons. +local function filter_warnings_on_new_line(check_result, normalized_options, line, next_index) + while true do + local warning = check_result.warnings[next_index] - if issue.code:match("1[24][23]") then - issue.field = get_field_string(issue) - end + if not warning or warning.line > line then + -- No more warnings on this line. + break + end - if issue.code == "561" then - issue.max_complexity = get_max_cyclomatic_complexity(opts, issue) - end - table.insert(res, issue) - end + if warning.code:find("^1") then + check_result.normalized_options[line] = normalized_options + elseif passes_filter(normalized_options, warning) then + table.insert(check_result.filtered_warnings, warning) end + + next_index = next_index + 1 end - return res + return next_index end --- Assumes `opts` are normalized. -local function filter_report(report) - local res = {} - - for i, file_report in ipairs(report) do - if not file_report.fatal then - res[i] = filter_file_report(file_report) - else - res[i] = file_report +-- Normalizing options is relatively expensive because full std definitions are quite large. +-- `CachingOptionsNormalizer` implements a caching layer that reduces number of `options.normalize` calls. +-- Caching is done based on identities of option tables. + +local CachingOptionsNormalizer = utils.class() + +function CachingOptionsNormalizer:__init() + self.result_trie = {} +end + +function CachingOptionsNormalizer:normalize_options(stds, option_stack) + local result_node = self.result_trie + + for _, option_table in ipairs(option_stack) do + if not result_node[option_table] then + result_node[option_table] = {} end + + result_node = result_node[option_table] end - return res + if result_node.result then + return result_node.result + end + + local result = options.normalize(option_stack, stds) + result_node.result = result + return result end +-- May mutate base_opts_stack. +local function filter_not_global_related_in_file(check_result, options_normalizer, stds, option_stack) + check_result.filtered_warnings = {} + check_result.normalized_options = {} --- Transforms file report, returning an array of pairs {issue, normalized options for the issue}. -local function annotate_file_report_with_affecting_options(file_report, option_stack, stds) - local opts = options.normalize(option_stack, stds) + -- Iterate over lines, warnings, and inline options at the same time, keeping opts_stack up to date. + local next_warning_index = 1 + local next_inline_option_index = 1 - if not opts.inline then - local res = {} - local issues = inline_options.get_issues(file_report.events) + for line in ipairs(check_result.line_lengths) do + next_inline_option_index = update_option_stack_for_new_line( + check_result, stds, option_stack, line, next_inline_option_index) + local normalized_options = options_normalizer:normalize_options(stds, option_stack) + check_line_length(check_result, normalized_options, line) + next_warning_index = filter_warnings_on_new_line(check_result, normalized_options, line, next_warning_index) + end +end - for i, issue in ipairs(issues) do - res[i] = {issue, opts} +local function may_have_options(opts_table) + for key in pairs(opts_table) do + if type(key) == "string" then + return true end - - return res end - local events, per_line_opts = inline_options.validate_options(file_report.events, file_report.per_line_options, stds) - local issues_with_inline_opts = inline_options.get_issues_and_affecting_options(events, per_line_opts) - - local normalized_options_cache = {} - local res = {} + return false +end - for i, pair in ipairs(issues_with_inline_opts) do - local issue, inline_opts = pair[1], pair[2] +local function get_option_stack(opts, file_index) + local res = {opts} - if not normalized_options_cache[inline_opts] then - normalized_options_cache[inline_opts] = options.normalize( - utils.concat_arrays({option_stack, inline_opts}), stds) + if opts and opts[file_index] then + -- Don't add useless per-file option tables, that messes up normalized option caching + -- since it memorizes based on option table identities. + if may_have_options(opts[file_index]) then + table.insert(res, opts[file_index]) end - res[i] = {issue, normalized_options_cache[inline_opts]} + for _, nested_opts in ipairs(opts[file_index]) do + table.insert(res, nested_opts) + end end return res end -local function get_option_stack(opts, report_index) - local res = {opts} +-- For each file check result: +-- * Stores invalid inline options, not filtered out not global-related warnings, and newly created line length warnings +-- in .filtered_warnings. +-- * Stores a map from line numbers to normalized options for lines of global-related warnings in .normalized_options. +local function filter_not_global_related(check_results, opts, stds) + local caching_options_normalizer = CachingOptionsNormalizer() + + for file_index, check_result in ipairs(check_results) do + if not check_result.fatal then + if check_result.warnings[1] and check_result.warnings[1].code == "011" then + -- Special case syntax errors, they don't have line numbers so normal filtering does not work. + check_result.filtered_warnings = check_result.warnings + check_result.normalized_options = {} + else + local base_file_option_stack = get_option_stack(opts, file_index) + filter_not_global_related_in_file(check_result, caching_options_normalizer, stds, base_file_option_stack) + end + end + end +end - if opts and opts[report_index] then - res[2] = opts[report_index] +-- 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. - for _, nested_opts in ipairs(opts[report_index]) do - table.insert(res, nested_opts) +local function is_definition(normalized_options, warning) + return normalized_options.allow_defined or (normalized_options.allow_defined_top and warning.top) +end + +-- Extracts sets of defined, exported and used globals from a file check result. +local function get_implicit_globals_in_file(check_result) + local defined = {} + local exported = {} + local used = {} + + for _, warning in ipairs(check_result.warnings) do + if warning.code:find("^11") then + if warning.code == "111" then + local normalized_options = check_result.normalized_options[warning.line] + + if is_definition(normalized_options, warning) then + if normalized_options.module then + defined[warning.name] = true + else + exported[warning.name] = true + end + end + else + used[warning.name] = true + end end end - return res + return defined, exported, used end -local function annotate_report_with_affecting_options(report, opts, stds) - local res = {} +-- Returns set of globals defines across all files except modules, a set of globals used across all files, +-- and an array of sets of globals defined per file, parallel to the check results array. +local function get_implicit_globals(check_results) + local globally_defined = {} + local globally_used = {} + local locally_defined = {} - for i, file_report in ipairs(report) do - if file_report.fatal then - res[i] = file_report - else - res[i] = annotate_file_report_with_affecting_options(file_report, get_option_stack(opts, i), stds) + for file_index, check_result in ipairs(check_results) do + if not check_result.fatal then + local defined, exported, used = get_implicit_globals_in_file(check_result) + utils.update(globally_defined, exported) + utils.update(globally_used, used) + locally_defined[file_index] = defined end end - return res + return globally_defined, globally_used, locally_defined end -local function add_long_line_warnings(report) - local res = {} +-- Mutates the warning and returns it or discards it by returning nothing if it's filtered out. +local function apply_implicit_definitions(globally_defined, globally_used, locally_defined, normalized_options, warning) + if not warning.code:find("^11") then + return warning + end + + if warning.code == "111" then + if normalized_options.module then + if locally_defined[warning.name] then + return + end - for i, file_report in ipairs(report) do - if file_report.fatal then - res[i] = file_report + warning.module = true else - res[i] = { - events = utils.update({}, file_report.events), - per_line_options = file_report.per_line_options - } - - for line_number, length in ipairs(file_report.line_lengths) do - -- `max_length` field will be added later, - -- `column` will be updated later. - table.insert(res[i].events, { - code = "631", - line = line_number, - column = 1, - line_ending = file_report.line_endings[line_number], - end_column = length - }) + if is_definition(normalized_options, warning) then + if globally_used[warning.name] then + return + end + + warning.code = "131" + warning.top = nil + else + if globally_defined[warning.name] then + return + end end + end + else + if globally_defined[warning.name] or locally_defined[warning.name] then + return + end + end + + return warning +end + +local function filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined) + for _, warning in ipairs(check_result.warnings) do + if warning.code:find("^1") then + local normalized_options = check_result.normalized_options[warning.line] + warning = apply_implicit_definitions( + globally_defined, globally_used, locally_defined, normalized_options, warning) - core_utils.sort_by_location(res[i].events) + if warning then + if warning.code:find("^11[12]") and not warning.module and + get_field_status(normalized_options, warning) == "read_only" then + warning.code = "12" .. warning.code:sub(3, 3) + elseif warning.code:find("^11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then + warning.code = "14" .. warning.code:sub(3, 3) + end + + if warning.code:match("11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then + warning.code = "14" .. warning.code:sub(3, 3) + end + + if passes_filter(normalized_options, warning) then + table.insert(check_result.filtered_warnings, warning) + end + end end end +end - return res +local function filter_global_related(check_results) + local globally_defined, globally_used, locally_defined = get_implicit_globals(check_results) + + for file_index, check_result in ipairs(check_results) do + if not check_result.fatal then + filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined[file_index]) + end + end 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, stds) - report = add_long_line_warnings(report) - report = annotate_report_with_affecting_options(report, opts, stds) - report = filter_implicit_defs(report) - return filter_report(report) +-- Processes an array of results of the check stage (or tables with .fatal field) into the final report. +-- `opts[i]`, if present, is used as options when processing `report[i]` together with options in its array part. +-- This function may mutate check results or reuse its parts in the return value. +function filter.filter(check_results, opts, stds) + filter_not_global_related(check_results, opts, stds) + filter_global_related(check_results) + + local report = {} + + for file_index, check_result in ipairs(check_results) do + if check_result.fatal then + report[file_index] = check_result + else + core_utils.sort_by_location(check_result.filtered_warnings) + report[file_index] = check_result.filtered_warnings + end + end + + return report end return filter diff -Nru luacheck-0.22.0/src/luacheck/format.lua luacheck-0.23.0/src/luacheck/format.lua --- luacheck-0.22.0/src/luacheck/format.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/format.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,126 +1,12 @@ +local stages = require "luacheck.stages" local utils = require "luacheck.utils" local format = {} local color_support = not utils.is_windows or os.getenv("ANSICON") -local function prefix_if_indirect(fmt) - return function(w) - if w.indirect then - return "indirectly " .. fmt - else - return fmt - end - end -end - -local function unused_or_overwritten(fmt) - return function(w) - if w.overwritten_line then - return fmt .. " is overwritten on line {overwritten_line} before use" - else - return fmt .. " is unused" - end - end -end - -local message_formats = { - ["011"] = "{msg}", - ["021"] = "{msg}", - ["022"] = "unpaired push directive", - ["023"] = "unpaired pop directive", - ["111"] = function(w) - if w.module then - return "setting non-module global variable {name!}" - else - return "setting non-standard global variable {name!}" - end - end, - ["112"] = "mutating non-standard global variable {name!}", - ["113"] = "accessing undefined variable {name!}", - ["121"] = "setting read-only global variable {name!}", - ["122"] = prefix_if_indirect("setting read-only field {field!} of global {name!}"), - ["131"] = "unused global variable {name!}", - ["142"] = prefix_if_indirect("setting undefined field {field!} of global {name!}"), - ["143"] = prefix_if_indirect("accessing undefined field {field!} of global {name!}"), - ["211"] = function(w) - if w.func then - if w.recursive then - return "unused recursive function {name!}" - elseif w.mutually_recursive then - return "unused mutually recursive function {name!}" - else - return "unused function {name!}" - end - else - return "unused variable {name!}" - end - end, - ["212"] = function(w) - if w.name == "..." then - return "unused variable length argument" - else - return "unused argument {name!}" - end - end, - ["213"] = "unused loop variable {name!}", - ["221"] = "variable {name!} is never set", - ["231"] = "variable {name!} is never accessed", - ["232"] = "argument {name!} is never accessed", - ["233"] = "loop variable {name!} is never accessed", - ["241"] = "variable {name!} is mutated but never accessed", - ["311"] = unused_or_overwritten("value assigned to variable {name!}"), - ["312"] = unused_or_overwritten("value of argument {name!}"), - ["313"] = unused_or_overwritten("value of loop variable {name!}"), - ["314"] = function(w) - local target = w.index and "index" or "field" - return "value assigned to " .. target .. " {field!} is overwritten on line {overwritten_line} before use" - end, - ["321"] = "accessing uninitialized variable {name!}", - ["331"] = "value assigned to variable {name!} is mutated but never accessed", - ["341"] = "mutating uninitialized variable {name!}", - ["411"] = "variable {name!} was previously defined on line {prev_line}", - ["412"] = "variable {name!} was previously defined as an argument on line {prev_line}", - ["413"] = "variable {name!} was previously defined as a loop variable on line {prev_line}", - ["421"] = "shadowing definition of variable {name!} on line {prev_line}", - ["422"] = "shadowing definition of argument {name!} on line {prev_line}", - ["423"] = "shadowing definition of loop variable {name!} on line {prev_line}", - ["431"] = "shadowing upvalue {name!} on line {prev_line}", - ["432"] = "shadowing upvalue argument {name!} on line {prev_line}", - ["433"] = "shadowing upvalue loop variable {name!} on line {prev_line}", - ["511"] = "unreachable code", - ["512"] = "loop is executed at most once", - ["521"] = "unused label {label!}", - ["531"] = "right side of assignment has more values than left side expects", - ["532"] = "right side of assignment has less values than left side expects", - ["541"] = "empty do..end block", - ["542"] = "empty if branch", - ["551"] = "empty statement", - ["561"] = function(w) - local template = "cyclomatic complexity of %s is too high ({complexity} > {max_complexity})" - - local function_descr - - if w.function_type == "main_chunk" then - function_descr = "main chunk" - elseif w.function_name then - function_descr = "{function_type} {function_name!}" - else - function_descr = "function" - end - - return template:format(function_descr) - end, - ["611"] = "line contains only whitespace", - ["612"] = "line contains trailing whitespace", - ["613"] = "trailing whitespace in a string", - ["614"] = "trailing whitespace in a comment", - ["621"] = "inconsistent indentation (SPACE followed by TAB)", - ["631"] = "line is too long ({end_column} > {max_length})" -} - local function get_message_format(warning) - local message_format = message_formats[warning.code] + local message_format = assert(stages.warnings[warning.code], "Unkown warning code " .. warning.code).message_format if type(message_format) == "function" then return message_format(warning) diff -Nru luacheck-0.22.0/src/luacheck/fs.lua luacheck-0.23.0/src/luacheck/fs.lua --- luacheck-0.22.0/src/luacheck/fs.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/fs.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,17 +1,8 @@ local fs = {} +local lfs = require "lfs" local utils = require "luacheck.utils" -fs.has_lfs = pcall(require, "lfs") - -local base_fs - -if fs.has_lfs then - base_fs = require "luacheck.lfs_fs" -else - base_fs = require "luacheck.lua_fs" -end - local function ensure_dir_sep(path) if path:sub(-1) ~= utils.dir_sep then return path .. utils.dir_sep @@ -105,11 +96,11 @@ end function fs.is_dir(path) - return base_fs.get_mode(path) == "directory" + return lfs.attributes(path, "mode") == "directory" end function fs.is_file(path) - return base_fs.get_mode(path) == "file" + return lfs.attributes(path, "mode") == "file" end -- Searches for file starting from path, going up until the file @@ -137,20 +128,30 @@ end end +-- Returns iterator over directory items or nil, error message. +function fs.dir_iter(dir_path) + local ok, iter, state, var = pcall(lfs.dir, dir_path) + + if not ok then + local err = utils.unprefix(iter, "cannot open " .. dir_path .. ": ") + return nil, "couldn't list directory: " .. err + end + + return iter, state, var +end + -- Returns list of all files in directory matching pattern. --- Returns nil, error message on error. +-- Additionally returns a mapping from directory paths that couldn't be expanded +-- to error messages. function fs.extract_files(dir_path, pattern) - assert(fs.has_lfs) local res = {} local err_map = {} local function scan(dir) - local ok, iter, state, var = pcall(base_fs.dir_iter, dir) + local iter, state, var = fs.dir_iter(dir) - if not ok then - local err = utils.unprefix(iter, "cannot open " .. dir .. ": ") - err = "couldn't recursively check: " .. err - err_map[dir] = err + if not iter then + err_map[dir] = state table.insert(res, dir) return end @@ -161,7 +162,6 @@ 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 @@ -176,13 +176,12 @@ -- Returns modification time for a file. function fs.get_mtime(path) - assert(fs.has_lfs) - return base_fs.get_mtime(path) + return lfs.attributes(path, "modification") end -- Returns absolute path to current working directory, with trailing directory separator. function fs.get_current_dir() - return ensure_dir_sep(base_fs.get_current_dir()) + return ensure_dir_sep(assert(lfs.currentdir())) end return fs diff -Nru luacheck-0.22.0/src/luacheck/init.lua luacheck-0.23.0/src/luacheck/init.lua --- luacheck-0.22.0/src/luacheck/init.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/init.lua 2018-09-18 19:43:27.000000000 +0000 @@ -5,7 +5,7 @@ local utils = require "luacheck.utils" local luacheck = { - _VERSION = "0.22.0" + _VERSION = "0.23.0" } local function raw_validate_options(fname, opts, stds, context) diff -Nru luacheck-0.22.0/src/luacheck/inline_options.lua luacheck-0.23.0/src/luacheck/inline_options.lua --- luacheck-0.22.0/src/luacheck/inline_options.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/inline_options.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,442 +0,0 @@ -local options = require "luacheck.options" -local core_utils = require "luacheck.core_utils" -local utils = require "luacheck.utils" - -local inline_options = {} - --- 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. --- Body can contain comments enclosed in balanced parens. - --- 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 - -local limit_opts = utils.array_to_set({"max_line_length", "max_code_line_length", "max_string_line_length", - "max_comment_line_length", "max_cyclomatic_complexity"}) - -local function is_valid_option_name(name) - if name == "std" or options.variadic_inline_options[name] then - return true - end - - name = name:gsub("^no_", "") - return options.nullary_inline_options[name] or limit_opts[name] -end - --- Splits a token array for an inline option invocation into --- option name and argument array, or nil if invocation is invalid. -local function split_invocation(tokens) - -- Name of the option can be split into several space separated tokens. - -- Since some valid names are prefixes of some other names - -- (e.g. `unused` and `unused arguments`), the longest prefix of token - -- array that is a valid option name should be considered. - local cur_name - local last_valid_name - local last_valid_name_end_index - - for i, token in ipairs(tokens) do - cur_name = cur_name and (cur_name .. "_" .. token) or token - - if is_valid_option_name(cur_name) then - last_valid_name = cur_name - last_valid_name_end_index = i - end - end - - if not last_valid_name then - return - end - - local args = {} - - for i = last_valid_name_end_index + 1, #tokens do - table.insert(args, tokens[i]) - end - - return last_valid_name, args -end - -local function unexpected_num_args(name, args, expected) - return ("inline option '%s' expects %d argument%s, %d given"):format( - name, expected, expected == 1 and "" or "s", #args) -end - --- Parses inline option body, returns options or nil and error message. -local function get_options(body) - local opts = {} - - local parts = utils.split(body, ",") - - for _, name_and_args in ipairs(parts) do - local tokens = utils.split(name_and_args) - local name, args = split_invocation(tokens) - - if not name then - if #tokens == 0 then - return nil, (#parts == 1) and "empty inline option" or "empty inline option invocation" - else - return nil, ("unknown inline option '%s'"):format(table.concat(tokens, " ")) - end - end - - if name == "std" then - if #args ~= 1 then - return nil, unexpected_num_args(name, args, 1) - end - - opts.std = args[1] - elseif name == "ignore" and #args == 0 then - opts.ignore = {".*"} - elseif options.variadic_inline_options[name] then - opts[name] = args - else - local full_name = name:gsub("_", " ") - local subs - name, subs = name:gsub("^no_", "") - local flag = subs == 0 - - if options.nullary_inline_options[name] then - if #args ~= 0 then - return nil, unexpected_num_args(full_name, args, 0) - end - - opts[name] = flag - else - assert(limit_opts[name]) - - if flag then - if #args ~= 1 then - return nil, unexpected_num_args(full_name, args, 1) - end - - local value = tonumber(args[1]) - - if not value then - return nil, ("inline option '%s' expects number as argument"):format(name) - end - - opts[name] = value - else - if #args ~= 0 then - return nil, unexpected_num_args(full_name, args, 0) - end - - opts[name] = false - end - end - end - end - - return opts -end - -local function invalid_options_error(event, msg) - return { - code = "021", - msg = msg, - line = event.line, - column = event.column, - end_column = event.end_column - } -end - -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, {[body] = true, line = location.line, column = location.column, end_column = end_column}) - - if after_push then - body = after_push - else - return - end - end - - local opts, err = get_options(body) - local event = {options = opts, line = location.line, column = location.column, end_column = end_column} - - if not opts then - table.insert(events, invalid_options_error(event, err)) - return - 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], event) - else - table.insert(events, event) - end -end - --- Adds inline options to events, marks invalid ones as errors. --- Returns map of per line inline option events (maps line numbers to arrays of event tables). -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 - -- Remove comments in balanced parens. - body = body:gsub("%b()", " ") - add_inline_option(events, per_line_opts, body, - comment.location, comment.end_column, code_lines[comment.location.line]) - end - end - - return per_line_opts, invalid_comments -end - -local function unpaired_boundary_error(event) - return { - code = "02" .. (event.push and "2" or "3"), - line = event.line, - column = event.column, - end_column = event.end_column - } -end - --- Given sorted events, transforms unpaired push and pop directives into errors. -local function mark_unpaired_boundaries(events) - local pushes = utils.Stack() - - for i, event in ipairs(events) do - if event.push then - pushes:push({index = i, event = event}) - elseif event.pop then - if pushes.size == 0 then - events[i] = unpaired_boundary_error(event) - elseif event.closure then - -- There could be unpaired push boundaries, pop them. - while not pushes.top.event.closure do - local unpaired_push = pushes:pop() - events[unpaired_push.index] = unpaired_boundary_error(unpaired_push.event) - end - - pushes:pop() - elseif pushes.top.event.closure then - -- User-supplied pop directive but last push is closure start. - events[i] = unpaired_boundary_error(event) - else - pushes:pop() - end - end - end - - -- Remaining push boundaries are unpaired. - for _, unpaired_push in ipairs(pushes) do - events[unpaired_push.index] = unpaired_boundary_error(unpaired_push.event) - end -end - --- Removes push/pop pairs that do no have any options inbetween. --- Returns new, sorted array of events. -local function filter_useless_boundaries(events) - local pushes = utils.Stack() - local filtered_events = {} - - for _, event in ipairs(events) do - if event.push then - table.insert(filtered_events, event) - pushes:push({filtered_index = #filtered_events, has_options = false}) - elseif event.pop then - local push = pushes:pop() - - if push.has_options then - table.insert(filtered_events, event) - else - table.remove(filtered_events, push.filtered_index) - end - else - if event.options and pushes.size ~= 0 then - pushes.top.has_options = true - end - - table.insert(filtered_events, event) - end - end - - return filtered_events -end - --- Adds events and errors related to inline options to the warning list. --- Returns a new list, sorted by location, plus a map of per line inline option events --- (maps line numbers to arrays of event tables). --- Inline option events are tables marked with `push`, `pop`, or `options` key. --- Push and pop events create and remove scopes that limit effects of inline options, --- and option events carry inline option tables themselves. --- Inline option errors have codes `02[123]`, issued for invalid option syntax, --- unpaired push directives and unpaired pop directives. -function inline_options.get_events(chstate) - local events = utils.update({}, chstate.warnings) - add_closure_boundaries(chstate.ast, events) - local per_line_opts = add_inline_options(events, chstate.comments, chstate.code_lines) - core_utils.sort_by_location(events) - mark_unpaired_boundaries(events) - events = filter_useless_boundaries(events) - return events, per_line_opts -end - -local function stack_to_array(stack) - local res = {} - - for i = 1, stack.size do - res[i] = stack[i] - end - - return res -end - --- Validates inline options within events and per-line options. --- Returns a new array of events and a new per-line option map --- with invalid options replaced with errors. --- This is required because of `std` option which has to be validated --- at join/filter time, not at check time, because of possible --- custom stds. -function inline_options.validate_options(events, per_line_opts, stds) - local new_events = {} - local new_per_line_opts = {} - local added_errors = false - - for i, event in ipairs(events) do - if event.options then - local ok, err = options.validate(options.all_options, event.options, stds) - - if ok then - new_events[i] = event - else - new_events[i] = invalid_options_error(event, err) - end - else - new_events[i] = event - end - end - - for line, line_events in pairs(per_line_opts) do - for _, event in ipairs(line_events) do - local ok, err = options.validate(options.all_options, event.options) - - if ok then - if not new_per_line_opts[line] then - new_per_line_opts[line] = {} - end - - table.insert(new_per_line_opts[line], event) - else - table.insert(new_events, invalid_options_error(event, err)) - added_errors = true - end - end - end - - -- This optimization is rather useless, it's mostly used here - -- to allow testing filtering without providing location information. - if added_errors then - core_utils.sort_by_location(new_events) - end - - return new_events, new_per_line_opts -end - --- Takes an array of events and a map of per-line options as returned from --- `get_events()`, possibly with location information stripped from push/pop events. --- Returns an array of pairs {issue, option_attay} that matches each --- warning or error with an array of inline option tables that affect it. --- Some option arrays may share identity. --- Returned array is sorted by warning location. -function inline_options.get_issues_and_affecting_options(events, per_line_opts) - local pushes = utils.Stack() - local option_stack = utils.Stack() - local res = {} - local empty_option_array = {} - - for _, event in ipairs(events) do - if event.code then - local option_array - - if option_stack.size == 0 then - option_array = empty_option_array - elseif option_stack.top.option_array then - option_array = option_stack.top.option_array - else - option_array = stack_to_array(option_stack) - option_stack.top.option_array = option_array - end - - if per_line_opts[event.line] then - local line_options = {} - - for i, inline_event in ipairs(per_line_opts[event.line]) do - line_options[i] = inline_event.options - end - - option_array = utils.concat_arrays({option_array, line_options}) - end - - table.insert(res, {event, option_array}) - elseif event.options then - option_stack:push(event.options) - elseif event.push then - -- New push boundary. Save size of the option stack to rollback later - -- when boundary is popped. - pushes:push(option_stack.size) - else - -- Rollback option stack. - local new_option_stack_size = pushes:pop() - - while option_stack.size ~= new_option_stack_size do - option_stack:pop() - end - end - end - - return res -end - --- Extract only warnings and errors from an array of events. -function inline_options.get_issues(events) - local res = {} - - for _, event in ipairs(events) do - if event.code then - table.insert(res, event) - end - end - - return res -end - -return inline_options diff -Nru luacheck-0.22.0/src/luacheck/lexer.lua luacheck-0.23.0/src/luacheck/lexer.lua --- luacheck-0.22.0/src/luacheck/lexer.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/lexer.lua 2018-09-18 19:43:27.000000000 +0000 @@ -4,7 +4,6 @@ local lexer = {} local sbyte = string.byte -local ssub = string.sub local schar = string.char local sreverse = string.reverse local tconcat = table.concat @@ -96,29 +95,34 @@ [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) +local function next_byte(state) + local offset = state.offset + 1 + state.offset = offset + return state.src:get_codepoint(offset) end -- Skipping helpers. -- Take the current character, skip something, return next character. local function skip_newline(state, newline) + local first_newline_offset = state.offset 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 + local line = state.line + local line_offsets = state.line_offsets + state.line_lengths[line] = first_newline_offset - line_offsets[line] + line = line + 1 + state.line = line + line_offsets[line] = state.offset return b end -local function skip_till_newline(state, b) - while not is_newline(b) and b ~= nil do +local function skip_to_newline(state, b) + while not is_newline(b) and b do b = next_byte(state) end @@ -166,7 +170,7 @@ while true do if is_newline(b) then -- Add the finished line. - lines[#lines+1] = ssub(state.src, line_start, state.offset-1) + lines[#lines+1] = state.src:get_substring(line_start, state.offset-1) b = skip_newline(state, b) line_start = state.offset @@ -185,8 +189,8 @@ end -- Add last line. - lines[#lines+1] = ssub(state.src, line_start, state.offset-opening_long_bracket-2) - next_byte(state) + lines[#lines+1] = state.src:get_substring(line_start, state.offset-opening_long_bracket-2) + state.offset = state.offset + 1 return token, tconcat(lines, "\n") end @@ -206,7 +210,7 @@ -- Put previous chunk into buffer. if chunk_start ~= state.offset then - chunks[#chunks+1] = ssub(state.src, chunk_start, state.offset-1) + chunks[#chunks+1] = state.src:get_substring(chunk_start, state.offset-1) end b = next_byte(state) @@ -359,16 +363,17 @@ 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) + chunks[#chunks+1] = state.src:get_substring(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) + string_value = state.src:get_substring(chunk_start, state.offset-1) end - next_byte(state) -- Skip the closing quote. + -- Skip the closing quote. + state.offset = state.offset + 1 return "string", string_value end @@ -439,36 +444,42 @@ -- Is it cdata literal? if b == BYTE_i or b == BYTE_I then -- It is complex literal. Skip "i" or "I". - next_byte(state) + state.offset = state.offset + 1 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) + local b1 = state.src:get_codepoint(state.offset+1) - 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) + if b1 == BYTE_l or b1 == BYTE_L then + local b2 = state.src:get_codepoint(state.offset+2) + + if b2 == BYTE_l or b2 == BYTE_L then + -- It is uint64_t literal. + state.offset = state.offset + 3 + end 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) + local b1 = state.src:get_codepoint(state.offset+1) if b1 == BYTE_l or b1 == BYTE_L then + local b2 = state.src:get_codepoint(state.offset+2) + if b2 == BYTE_u or b2 == BYTE_U then -- It is uint64_t literal. - next_byte(state, 3) + state.offset = state.offset + 3 else -- It is int64_t literal. - next_byte(state, 2) + state.offset = state.offset + 2 end end end end end - return "number", ssub(state.src, start, state.offset-1) + return "number", state.src:get_substring(start, state.offset-1) end local function lex_ident(state) @@ -479,7 +490,7 @@ b = next_byte(state) end - local ident = ssub(state.src, start, state.offset-1) + local ident = state.src:get_substring(start, state.offset-1) if keywords[ident] then return ident @@ -494,27 +505,26 @@ -- Is it "-" or comment? if b ~= BYTE_DASH then return "-" - else - -- It is a comment. - b = next_byte(state) - local start = state.offset + end - -- Is it a long comment? - if b == BYTE_OBRACK then - local long_bracket - b, long_bracket = skip_long_bracket(state) + -- It is a comment. + b = next_byte(state) + local start = state.offset - if b == BYTE_OBRACK then - return lex_long_string(state, long_bracket, "comment") - end - end + -- Is it a long comment? + if b == BYTE_OBRACK then + local long_bracket + b, long_bracket = skip_long_bracket(state) - -- 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 + if b == BYTE_OBRACK then + return lex_long_string(state, long_bracket, "long_comment") + end end + + -- Short comment. + skip_to_newline(state, b) + local comment_value = state.src:get_substring(start, state.offset - 1) + return "short_comment", comment_value end local function lex_bracket(state) @@ -534,7 +544,7 @@ local b = next_byte(state) if b == BYTE_EQ then - next_byte(state) + state.offset = state.offset + 1 return "==" else return "=" @@ -545,10 +555,10 @@ local b = next_byte(state) if b == BYTE_EQ then - next_byte(state) + state.offset = state.offset + 1 return "<=" elseif b == BYTE_LT then - next_byte(state) + state.offset = state.offset + 1 return "<<" else return "<" @@ -559,10 +569,10 @@ local b = next_byte(state) if b == BYTE_EQ then - next_byte(state) + state.offset = state.offset + 1 return ">=" elseif b == BYTE_GT then - next_byte(state) + state.offset = state.offset + 1 return ">>" else return ">" @@ -573,7 +583,7 @@ local b = next_byte(state) if b == BYTE_SLASH then - next_byte(state) + state.offset = state.offset + 1 return "//" else return "/" @@ -584,7 +594,7 @@ local b = next_byte(state) if b == BYTE_EQ then - next_byte(state) + state.offset = state.offset + 1 return "~=" else return "~" @@ -595,7 +605,7 @@ local b = next_byte(state) if b == BYTE_COLON then - next_byte(state) + state.offset = state.offset + 1 return "::" else return ":" @@ -609,21 +619,27 @@ b = next_byte(state) if b == BYTE_DOT then - next_byte(state) + state.offset = state.offset + 1 return "...", "..." else return ".." end elseif b and to_dec(b) then -- Backtrack to dot. - return lex_number(state, next_byte(state, -1)) + state.offset = state.offset - 2 + return lex_number(state, next_byte(state)) else return "." end end local function lex_any(state, b) - next_byte(state) + state.offset = state.offset + 1 + + if b > 255 then + b = 255 + end + return schar(b) end @@ -659,62 +675,75 @@ 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) +function lexer.new_state(src, line_offsets, line_lengths) local state = { src = src, line = 1, - line_offset = 1, + line_offsets = line_offsets or {}, + line_lengths = line_lengths or {}, offset = 1 } - if ssub(src, 1, 2) == "#!" then - -- Skip shebang. - skip_newline(state, skip_till_newline(state, next_byte(state, 2))) + state.line_offsets[1] = 1 + + if src:get_length() >= 2 and src:get_substring(1, 2) == "#!" then + -- Skip shebang line. + state.offset = 2 + skip_to_newline(state, next_byte(state)) end return state 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.get_quoted_substring_or_line(state, line, offset, end_offset) + local line_length = state.line_lengths[line] + + if line_length then + local line_end_offset = state.line_offsets[line] + line_length - 1 + + if line_end_offset < end_offset then + end_offset = line_end_offset + end + end + + return "'" .. state.src:get_printable_substring(offset, end_offset) .. "'" +end + +-- Looks for next token starting from state.line, state.offset. +-- Returns next token, its value and its location (line, offset). +-- Sets state.line, state.offset to token end location + 1. +-- Fills state.line_offsets and state.line_lengths. +-- On error returns nil, error message, error location (line, offset), error end offset. function lexer.next_token(state) - local b = skip_space(state, sbyte(state.src, state.offset)) + local line_offsets = state.line_offsets + local b = skip_space(state, state.src:get_codepoint(state.offset)) -- Save location of token start. local token_line = state.line - local token_column = state.offset - state.line_offset + 1 + local line_offset = line_offsets[token_line] 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 + if not b then + -- EOF token has length 1. + state.offset = state.offset + 1 + state.line_lengths[token_line] = token_offset - line_offset + return "eof", nil, token_line, token_offset + end + + local token, token_value, relative_error_offset = (byte_handlers[b] or lex_any)(state, b) + + if relative_error_offset then + -- Error relative to current offset. + local error_offset = state.offset + relative_error_offset + local error_end_offset = math.min(state.offset, state.src:get_length()) + local error_message = token_value .. " " .. lexer.get_quoted_substring_or_line(state, + state.line, error_offset, error_end_offset) + return nil, error_message, state.line, error_offset, error_end_offset end - return token, token_value, token_line, token_column, token_offset, err_end_column or token_column + -- Single character errors fall through here. + return token, token_value, token_line, token_offset, not token and token_offset end return lexer diff -Nru luacheck-0.22.0/src/luacheck/lfs_fs.lua luacheck-0.23.0/src/luacheck/lfs_fs.lua --- luacheck-0.22.0/src/luacheck/lfs_fs.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/lfs_fs.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -local lfs = require "lfs" - -local lfs_fs = {} - -function lfs_fs.get_mode(path) - return lfs.attributes(path, "mode") -end - -function lfs_fs.get_current_dir() - return assert(lfs.currentdir()) -end - -function lfs_fs.get_mtime(path) - return lfs.attributes(path, "modification") -end - -lfs_fs.dir_iter = lfs.dir - -return lfs_fs diff -Nru luacheck-0.22.0/src/luacheck/linearize.lua luacheck-0.23.0/src/luacheck/linearize.lua --- luacheck-0.22.0/src/luacheck/linearize.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/linearize.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,777 +0,0 @@ -local parser = require "luacheck.parser" -local utils = require "luacheck.utils" - -local function new_unused_field_value_warning(node, overwriting_node) - return { - code = "314", - field = node.field, - index = node.is_index, - overwritten_line = overwriting_node.location.line, - overwritten_column = overwriting_node.location.column, - overwritten_end_column = overwriting_node.location.column + #overwriting_node.first_token - 1, - line = node.location.line, - column = node.location.column, - end_column = node.location.column + #node.first_token - 1 - } -end - -local type_codes = { - var = "1", - func = "1", - arg = "2", - loop = "3", - loopi = "3" -} - -local function new_redefined_warning(var, prev_var, is_same_scope) - return { - code = "4" .. (is_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, - end_column = var.location.column + (var.self and #":" or #var.name) - 1, - self = var.self and prev_var.self, - prev_line = prev_var.location.line, - prev_column = prev_var.location.column, - prev_end_column = prev_var.location.column + (prev_var.self and #":" or #prev_var.name) - 1 - } -end - -local function new_unused_label_warning(label) - return { - code = "521", - label = label.name, - line = label.location.line, - column = label.location.column, - end_column = label.end_column - } -end - -local function new_unbalanced_assignment_warning(node) - local has_shorter_lhs = #node[1] < #node[2] - - return { - code = "53" .. (has_shorter_lhs and "1" or "2"), - line = node.equals_location.line, - column = node.equals_location.column, - end_column = node.equals_location.column - } -end - -local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"}) - -local function new_line(node, parent, value) - return { - accessed_upvalues = {}, -- Maps variables to arrays of accessing items. - mutated_upvalues = {}, -- Maps variables to arrays of mutating items. - set_upvalues = {}, -- Maps variables to arays of setting items. - lines = {}, - node = node, - parent = parent, - value = value, - 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, item, is_init) - local value = { - var = var_node.var, - location = var_node.location, - type = is_init and var_node.var.type or "var", - initial = is_init, - node = value_node, - using_lines = {}, - empty = is_init and not value_node and (var_node.var.type == "var"), - item = item - } - - if value_node and value_node.tag == "Function" then - value.type = "func" - value_node.value = value - end - - return value -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 = {}, - mutations = {}, - 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 - parser.syntax_error( - goto_.location, goto_.location.column + 4, "'break' is not inside a loop") - else - parser.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 - table.insert(self.chstate.warnings, new_unused_label_warning(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 is_same_scope = self.scopes.top.vars[var.name] - - if var.name ~= "..." then - table.insert(self.chstate.warnings, new_redefined_warning(var, prev_var, is_same_scope)) - end - - if is_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) - if not node.var then - node.var = self:resolve_var(node[1]) - end - - return node.var -end - -function LinState:register_label(name, location, end_column) - local prev_label = self.scopes.top.labels[name] - - if prev_label then - assert(not pseudo_labels[name]) - parser.syntax_error(location, end_column, ("label '%s' already defined on line %d"):format( - name, prev_label.location.line), prev_label.location, prev_label.end_column) - end - - self.scopes.top.labels[name] = new_label(self.lines.top, name, location, end_column) -end - -function LinState:check_assignment_balance(node) - local lhs = node[1] - local rhs = node[2] - - if not rhs then - return - end - - if (#lhs < #rhs) or ((#lhs > #rhs) and node.tag == "Set" and not is_unpacking(rhs[#rhs])) then - table.insert(self.chstate.warnings, new_unbalanced_assignment_warning(node)) - end -end - --- Block is either a `Do` statement or a `Then` block within an `If` statement. -function LinState:check_empty_block(block) - if #block == 0 then - local is_do_block = block.tag == "Do" - - table.insert(self.chstate.warnings, { - code = "54" .. (is_do_block and "1" or "2"), - line = block.location.line, - column = block.location.column, - end_column = block.location.column + (is_do_block and #"do" or #"then") - 1 - }) - 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_assignment_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_assignment_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) - - if var then - self:register_upvalue_action(item, var, "set_upvalues") - end - else - assert(expr.tag == "Index") - self:scan_lhs_index(item, expr) - 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, key) - 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, "accessed_upvalues") -end - -function LinState:mark_mutation(item, node) - node.var.mutated = true - - if not item.mutations[node.var] then - item.mutations[node.var] = {} - end - - table.insert(item.mutations[node.var], node) - self:register_upvalue_action(item, node.var, "mutated_upvalues") -end - -function LinState:scan_expr_Id(item, node) - if self:check_var(node) then - self:mark_access(item, node) - end -end - -function LinState:scan_expr_Dots(item, node) - local dots = self:check_var(node) - - if not dots or dots.line ~= self.lines.top then - parser.syntax_error(node.location, node.location.column + 2, "cannot use '...' outside a vararg function") - end - - self:mark_access(item, node) -end - -function LinState:scan_lhs_index(item, node) - if node[1].tag == "Id" then - if self:check_var(node[1]) then - self:mark_mutation(item, node[1]) - end - elseif node[1].tag == "Index" then - self:scan_lhs_index(item, node[1]) - else - self:scan_expr(item, node[1]) - end - - self:scan_expr(item, node[2]) -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 - -local function node_to_lua_value(node) - if node.tag == "True" then - return true, "true" - elseif node.tag == "False" then - return false, "false" - elseif node.tag == "String" then - return node[1], node[1] - elseif node.tag == "Number" then - local str = node[1] - - if str:find("[iIuUlL]") then - -- Ignore LuaJIT cdata literals. - return - end - - -- On Lua 5.3 convert to float to get same results as on Lua 5.1 and 5.2. - if _VERSION == "Lua 5.3" and not str:find("[%.eEpP]") then - str = str .. ".0" - end - - local number = tonumber(str) - - if number and number == number and number < 1/0 and number > -1/0 then - return number, node[1] - end - end -end - -function LinState:scan_expr_Table(item, node) - local array_index = 1.0 - local key_to_node = {} - - for _, pair in ipairs(node) do - local key, field - - if pair.tag == "Pair" then - key, field = node_to_lua_value(pair[1]) - self:scan_exprs(item, pair) - else - key = array_index - field = tostring(math.floor(key)) - array_index = array_index + 1.0 - self:scan_expr(item, pair) - end - - if field then - if key_to_node[key] then - table.insert(self.chstate.warnings, new_unused_field_value_warning(key_to_node[key], pair)) - end - - key_to_node[key] = pair - pair.field = field - pair.is_index = pair.tag ~= "Pair" or nil - end - end -end - -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, 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(node) - self.lines:push(new_line(node, self.lines.top)) - self:enter_scope() - self:emit(new_local_item(node[1])) - self:enter_scope() - self:register_vars(node[1], "arg") - self:emit_stmts(node[2]) - 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) - 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 assigns it as `chstate.main_line`. --- Adds warnings: redefined/shadowed, unused field, unused label, unbalanced assignment, empty block. -local function linearize(chstate) - local linstate = LinState(chstate) - chstate.main_line = linstate:build_line({{{tag = "Dots", "..."}}, chstate.ast}) - assert(linstate.lines.size == 0) - assert(linstate.scopes.size == 0) -end - -return linearize diff -Nru luacheck-0.22.0/src/luacheck/love_standard.lua luacheck-0.23.0/src/luacheck/love_standard.lua --- luacheck-0.22.0/src/luacheck/love_standard.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/love_standard.lua 2018-09-18 19:43:27.000000000 +0000 @@ -10,12 +10,14 @@ directorydropped = read_write, draw = read_write, errhand = read_write, + errorhandler = read_write, filedropped = read_write, focus = read_write, gamepadaxis = read_write, gamepadpressed = read_write, gamepadreleased = read_write, handlers = read_write, + hasDeprecationOutput = empty, joystickadded = read_write, joystickaxis = read_write, joystickhat = read_write, @@ -33,6 +35,7 @@ quit = read_write, resize = read_write, run = read_write, + setDeprecationOutput = empty, textedited = read_write, textinput = read_write, threaderror = read_write, @@ -44,9 +47,13 @@ wheelmoved = read_write, audio = standards.def_fields("getDistanceModel","getDopplerScale","getSourceCount","getOrientation", - "getPosition","getVelocity","getVolume","newSource","pause","play","resume","rewind", - "setDistanceModel","setDopplerScale","setOrientation","setPosition","setVelocity", - "setVolume","stop"), + "getPosition","getVelocity","getVolume","newSource","pause","play","setDistanceModel","setDopplerScale", + "setOrientation","setPosition","setVelocity", "setVolume","stop","getActiveSourceCount","getRecordingDevices", + "newQueueableSource","setEffect","getActiveEffects","getEffect","getMaxSceneEffects","getMaxSourceEffects", + "isEffectsSupported","setMixWithSystem"), + + data = standards.def_fields("compress","decode","decompress","encode","getPackedSize","hash","newByteData", + "newDataView","pack","unpack"), event = standards.def_fields("clear","poll","pump","push","quit","wait"), @@ -55,36 +62,46 @@ "getRealDirectory","getRequirePath","getSaveDirectory","getSize","getSource", "getSourceBaseDirectory","getUserDirectory","getWorkingDirectory","init","isDirectory", "isFile","isFused","isSymlink","lines","load","mount","newFile","newFileData","read", - "remove","setIdentity","setRequirePath","setSource","setSymlinksEnabled","unmount","write"), + "remove","setIdentity","setRequirePath","setSource","setSymlinksEnabled","unmount","write", + "getInfo","setCRequirePath","getCRequirePath"), + + font = standards.def_fields("newImageRasterizer", "newGlyphData", "newTrueTypeRasterizer", + "newBMFontRasterizer", "newRasterizer"), graphics = standards.def_fields("arc","circle","clear","discard","draw","ellipse","getBackgroundColor", "getBlendMode","getCanvas","getCanvasFormats","getColor","getColorMask", - "getCompressedImageFormats","getDefaultFilter","getDimensions","getFont","getHeight", + "getDefaultFilter","getDimensions","getFont","getHeight", "getLineJoin","getLineStyle","getLineWidth","getShader","getStats","getStencilTest", "getSupported","getSystemLimits","getPointSize","getRendererInfo","getScissor","getWidth", "intersectScissor","isGammaCorrect","isWireframe","line","newCanvas","newFont","newMesh", "newImage","newImageFont","newParticleSystem","newShader","newText","newQuad", - "newScreenshot","newSpriteBatch","newVideo","origin","points","polygon","pop","present", + "newSpriteBatch","newVideo","origin","points","polygon","pop","present", "print","printf","push","rectangle","reset","rotate","scale","setBackgroundColor", "setBlendMode","setCanvas","setColor","setColorMask","setDefaultFilter","setFont", "setLineJoin","setLineStyle","setLineWidth","setNewFont","setShader","setPointSize", - "setScissor","setStencilTest","setWireframe","shear","stencil","translate"), + "setScissor","setStencilTest","setWireframe","shear","stencil","translate", + "captureScreenshot","getImageFormats","newArrayImage","newVolumeImage","newCubeImage", + "getTextureTypes","drawLayer","setDepthMode","setMeshCullMode","setFrontFaceWinding", + "applyTransform","replaceTransform","transformPoint","inverseTransformPoint","getStackDepth", + "flushBatch","validateShader","drawInstanced","getDepthMode","getFrontFaceWinding","getMeshCullMode", + "getDPIScale","getPixelDimensions","getPixelHeight","getPixelWidth","isActive","getDefaultMipmapFilter", + "setDefaultMipmapFilter"), - image = standards.def_fields("isCompressed","newCompressedData","newImageData"), + image = standards.def_fields("isCompressed","newCompressedData","newImageData","newCubeFaces"), joystick = standards.def_fields("getJoystickCount","getJoysticks","loadGamepadMappings", "saveGamepadMappings","setGamepadMapping"), keyboard = standards.def_fields("getKeyFromScancode","getScancodeFromKey","hasKeyRepeat","hasTextInput", - "isDown","isScancodeDown","setKeyRepeat","setTextInput"), + "isDown","isScancodeDown","setKeyRepeat","setTextInput","hasScreenKeyboard"), math = standards.def_fields("compress","decompress","gammaToLinear","getRandomSeed","getRandomState", "isConvex","linearToGamma","newBezierCurve","newRandomGenerator","noise","random", - "randomNormal","setRandomSeed","setRandomState","triangulate"), + "randomNormal","setRandomSeed","setRandomState","triangulate","newTransform"), mouse = standards.def_fields("getCursor","getPosition","getRelativeMode","getSystemCursor","getX", "getY","hasCursor","isDown","isGrabbed","isVisible","newCursor","setCursor","setGrabbed", - "setPosition","setRelativeMode","setVisible","setX","setY"), + "setPosition","setRelativeMode","setVisible","setX","setY","isCursorSupported"), physics = standards.def_fields("getDistance","getMeter","newBody","newChainShape","newCircleShape", "newDistanceJoint","newEdgeShape","newFixture","newFrictionJoint","newGearJoint", @@ -95,7 +112,7 @@ sound = standards.def_fields("newDecoder","newSoundData"), system = standards.def_fields("getClipboardText","getOS","getPowerInfo","getProcessorCount","openURL", - "setClipboardText","vibrate"), + "setClipboardText","vibrate","hasBackgroundMusic"), thread = standards.def_fields("getChannel","newChannel","newThread"), @@ -109,7 +126,8 @@ "getFullscreenModes","getIcon","getMode","getPixelScale","getPosition","getTitle", "hasFocus","hasMouseFocus","isDisplaySleepEnabled","isMaximized","isOpen","isVisible", "maximize","minimize","requestAttention","setDisplaySleepEnabled","setFullscreen", - "setIcon","setMode","setPosition","setTitle","showMessageBox","toPixels") + "setIcon","setMode","setPosition","setTitle","showMessageBox","toPixels", + "updateMode","isMinimized","restore","getDPIScale") } } diff -Nru luacheck-0.22.0/src/luacheck/lua_fs.lua luacheck-0.23.0/src/luacheck/lua_fs.lua --- luacheck-0.22.0/src/luacheck/lua_fs.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/lua_fs.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,58 +0,0 @@ -local utils = require "luacheck.utils" - -local lua_fs = {} - --- Quotes an argument for a command for os.execute or io.popen. --- Same code has been contributed to pl. -local function quote_arg(argument) - if utils.is_windows then - if argument == "" or argument:find('[ \f\t\v]') then - -- Need to quote the argument. - -- Quotes need to be escaped with backslashes; - -- additionally, backslashes before a quote, escaped or not, - -- need to be doubled. - -- See documentation for CommandLineToArgvW Windows function. - argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"' - end - - -- os.execute() uses system() C function, which on Windows passes command - -- to cmd.exe. Escape its special characters. - return (argument:gsub('["^<>!|&%%]', "^%0")) - else - if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then - -- To quote arguments on posix-like systems use single quotes. - -- To represent an embedded single quote close quoted string ('), - -- add escaped quote (\'), open quoted string again ('). - argument = "'" .. argument:gsub("'", [['\'']]) .. "'" - end - - return argument - end -end - -local mode_cmd_template - -if utils.is_windows then - mode_cmd_template = [[if exist %s\* (echo directory) else (if exist %s echo file)]] -else - mode_cmd_template = [[if [ -d %s ]; then echo directory; elif [ -f %s ]; then echo file; fi]] -end - -function lua_fs.get_mode(path) - local quoted_path = quote_arg(path) - local fh = assert(io.popen(mode_cmd_template:format(quoted_path, quoted_path))) - local mode = fh:read("*a"):match("^(%S*)") - fh:close() - return mode -end - -local pwd_cmd = utils.is_windows and "cd" or "pwd" - -function lua_fs.get_current_dir() - local fh = assert(io.popen(pwd_cmd)) - local current_dir = fh:read("*a"):gsub("\n$", "") - fh:close() - return current_dir -end - -return lua_fs diff -Nru luacheck-0.22.0/src/luacheck/main.lua luacheck-0.23.0/src/luacheck/main.lua --- luacheck-0.22.0/src/luacheck/main.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/main.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,9 +1,8 @@ -local argparse = require "luacheck.argparse" -local builtin_standards = require "luacheck.builtin_standards" +local argparse = require "argparse" local config = require "luacheck.config" -local fs = require "luacheck.fs" local luacheck = require "luacheck" local multithreading = require "luacheck.multithreading" +local profiler = require "luacheck.profiler" local runner = require "luacheck.runner" local utils = require "luacheck.utils" local version = require "luacheck.version" @@ -30,14 +29,7 @@ Luacheck documentation: https://luacheck.readthedocs.org]]) :help_max_width(80) - local lfs_dir_notice = "" - - if not fs.has_lfs then - lfs_dir_notice = "\nWarning: LuaFileSystem not found, directory checking disabled." - end - - parser:argument "files" - :description("List of files, directories and rockspecs to check. Pass '-' to check stdin." .. lfs_dir_notice) + parser:argument("files", "List of files, directories and rockspecs to check. Pass '-' to check stdin.") :args "+" :argname "" @@ -78,17 +70,10 @@ :action "concat" :init(nil)) - local default_std_name = "max" - - for _, name in ipairs({"lua51c", "lua52c", "lua53c", "luajit"}) do - if builtin_standards._G == builtin_standards[name] then - default_std_name = name - break - end - end - parser:group("Options for configuring allowed globals", - parser:option("--std", ("Set standard globals, default is _G. can be one of:\n" .. + parser:option("--std", "Set standard globals, default is max. can be one of:\n" .. + " max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" .. + " min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" .. " lua51 - globals of Lua 5.1 without deprecated ones;\n" .. " lua51c - globals of Lua 5.1;\n" .. " lua52 - globals of Lua 5.2;\n" .. @@ -97,16 +82,14 @@ " lua53c - globals of Lua 5.3 with LUA_COMPAT_5_2;\n" .. " luajit - globals of LuaJIT 2.x;\n" .. " ngx_lua - globals of Openresty lua-nginx-module 0.10.10, including standard LuaJIT 2.x globals;\n" .. - " min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" .. - " max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" .. - " _G - same as lua51c, lua52c, lua53c, or luajit depending on version of Lua used to run luacheck " .. - "or same as max if couldn't detect the version. Currently %s;\n" .. - " love - globals added by LOVE (love2d);\n" .. - " busted - globals added by Busted 2.0;\n" .. - " rockspec - globals allowed in rockspecs;\n" .. + " love - globals added by LÖVE;\n" .. + " busted - globals added by Busted 2.0, by default added for files ending with _spec.lua within spec, " .. + "test, and tests subdirectories;\n" .. + " rockspec - globals allowed in rockspecs, by default added for files ending with .rockspec;\n" .. + " luacheckrc - globals allowed in Luacheck configs, by default added for files ending with .luacheckrc;\n" .. " none - no standard globals.\n\n" .. "Sets can be combined using '+'. Extra sets can be defined in config by " .. - "adding to `stds` global."):format(default_std_name)), + "adding to `stds` global in config."), parser:flag("-c --compat", "Equivalent to --std max."), parser:option("--globals", "Add custom global variables (e.g. foo) or fields (e.g. foo.bar) " .. @@ -183,8 +166,6 @@ :action "store_false" :target "max_cyclomatic_complexity" - parser:flag("--no-inline", "Disable inline options."):target("inline"):action("store_false") - local default_global_path = config.get_default_global_path() local config_opt = parser:option("--config", "Path to configuration file. (default: "..config.default_path..")") @@ -225,13 +206,7 @@ parser:option("--filename", "Use another filename in output and for selecting configuration overrides.") - local lfs_cache_notice = "" - - if not fs.has_lfs then - lfs_cache_notice = "\nWarning: LuaFileSystem not found, caching disabled." - end - - local cache_opt = parser:option("--cache", "Path to cache file (default: .luacheckcache)." .. lfs_cache_notice) + local cache_opt = parser:option("--cache", "Path to cache file (default: .luacheckcache).") :args "?" local no_cache_opt = parser:flag("--no-cache", "Do not use cache.") @@ -270,6 +245,8 @@ :action "store_false" :target "color") + parser:flag("--profile", "Show performance statistics."):hidden(true) + parser:flag("-v --version", "Show version info and exit.") :action(function() print(version.string) os.exit(exit_codes.ok) end) @@ -284,16 +261,17 @@ os.exit(exit_codes.critical) end + if args.profile then + args.jobs = 1 + profiler.init() + end + if args.quiet == 0 then args.quiet = nil end if args.cache then - if fs.has_lfs then - args.cache = args.cache[1] or true - else - args.cache = nil - end + args.cache = args.cache[1] or true end local checker, err, is_invalid_args_error = runner.new(args) @@ -343,6 +321,10 @@ io.stdout:write(output) + if args.profile then + profiler.report() + end + if report.fatals > 0 then os.exit(exit_codes.fatals) elseif report.errors > 0 then diff -Nru luacheck-0.22.0/src/luacheck/name_functions.lua luacheck-0.23.0/src/luacheck/name_functions.lua --- luacheck-0.22.0/src/luacheck/name_functions.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/name_functions.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -local function get_index_name(base_name, key_node) - if key_node.tag == "String" then - return base_name .. "." .. key_node[1] - end -end - -local function get_full_field_name(node) - if node.tag == "Id" then - return node[1] - elseif node.tag == "Index" then - local base_name = get_full_field_name(node[1]) - return base_name and get_index_name(base_name, node[2]) - end -end - -local handle_node - -local function handle_nodes(nodes) - for _, node in ipairs(nodes) do - if type(node) == "table" then - handle_node(node) - end - end -end - -function handle_node(node, name) - if node.tag == "Function" then - node.name = name - handle_nodes(node[2]) - elseif node.tag == "Set" or node.tag == "Local" then - local lhs = node[1] - local rhs = node[2] - - -- No need to handle LHS if there is no RHS, it's always just a list of locals in that case. - if rhs then - handle_nodes(lhs) - - for index, rhs_node in ipairs(rhs) do - local lhs_node = lhs[index] - local field_name = lhs_node and get_full_field_name(lhs_node) - handle_node(rhs_node, field_name) - end - end - elseif node.tag == "Localrec" then - handle_node(node[2], node[1][1]) - elseif node.tag == "Table" and name then - for _, pair_node in ipairs(node) do - if pair_node.tag == "Pair" then - local key_node = pair_node[1] - local value_node = pair_node[2] - handle_node(key_node) - handle_node(value_node, get_index_name(name, key_node)) - else - handle_node(pair_node) - end - end - else - handle_nodes(node) - end -end - --- Adds `name` field to `Function` ast nodes when possible: --- * Function assigned to a variable (doesn't matter if local or global): "foo". --- * Function assigned to a field: "foo.bar.baz". --- Function can be in a table assigned to a variable or a field, e.g. `foo.bar = {baz = function() ... end}`. --- * Otherwise: `nil`. -local function name_functions(chstate) - handle_nodes(chstate.ast) -end - -return name_functions diff -Nru luacheck-0.22.0/src/luacheck/options.lua luacheck-0.23.0/src/luacheck/options.lua --- luacheck-0.22.0/src/luacheck/options.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/options.lua 2018-09-18 19:43:27.000000000 +0000 @@ -79,8 +79,7 @@ max_code_line_length = number_or_false, max_string_line_length = number_or_false, max_comment_line_length = number_or_false, - max_cyclomatic_complexity = number_or_false, - inline = boolean + max_cyclomatic_complexity = number_or_false } utils.update(options.all_options, options.nullary_inline_options) @@ -148,7 +147,7 @@ end end - table.insert(add_stds, 1, base_std or stds._G) + table.insert(add_stds, 1, base_std or stds.max) return add_stds end @@ -389,7 +388,6 @@ local scalar_options = { unused_secondaries = true, self = true, - inline = true, module = false, allow_defined = false, allow_defined_top = false, @@ -399,7 +397,7 @@ -- Returns normalized options. -- Normalized options have fields: -- std: normalized std table, see `luacheck.standards` module; --- unused_secondaries, self, inline, module, allow_defined, allow_defined_top: booleans; +-- unused_secondaries, self, module, allow_defined, allow_defined_top: booleans; -- max_line_length: number or false; -- rules: see get_rules. function options.normalize(opts_stack, stds) diff -Nru luacheck-0.22.0/src/luacheck/parser.lua luacheck-0.23.0/src/luacheck/parser.lua --- luacheck-0.22.0/src/luacheck/parser.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/parser.lua 2018-09-18 19:43:27.000000000 +0000 @@ -3,120 +3,97 @@ local parser = {} -local function new_state(src) - return { - lexer = lexer.new_state(src), - code_lines = {}, -- Set of line numbers containing code. - line_endings = {}, -- Maps line numbers to "comment", "string", or nil based on whether - -- the line ending is within a token. - comments = {}, -- Array of {comment = string, location = location}. - hanging_semicolons = {} -- Array of locations of semicolons not following a statement. - } -end - -local function location(state) - return { - line = state.line, - column = state.column, - offset = state.offset - } -end +-- A table with range info, or simply range, has `line`, `offset`, and `end_offset` fields. +-- `line` is the line of the first character. +-- Parser state table has range info for the current token, and all AST +-- node tables have range info for themself, including parens around expressions +-- that are otherwise not reflected in the AST structure. parser.SyntaxError = utils.class() -function parser.SyntaxError:__init(loc, end_column, msg, prev_loc, prev_end_column) - self.line = loc.line - self.column = loc.column - self.end_column = end_column +function parser.SyntaxError:__init(msg, range, prev_range) self.msg = msg + self.line = range.line + self.offset = range.offset + self.end_offset = range.end_offset - if prev_loc then - self.prev_line = prev_loc.line - self.prev_column = prev_loc.column - self.prev_end_column = prev_end_column + if prev_range then + self.prev_line = prev_range.line + self.prev_offset = prev_range.offset + self.prev_end_offset = prev_range.end_offset end end -function parser.syntax_error(loc, end_column, msg, prev_loc, prev_end_column) - error(parser.SyntaxError(loc, end_column, msg, prev_loc, prev_end_column), 0) -end - -local function token_body_or_line(state) - return state.lexer.src:sub(state.offset, state.lexer.offset - 1):match("^[^\r\n]*") +function parser.syntax_error(msg, range, prev_range) + error(parser.SyntaxError(msg, range, prev_range), 0) end -local function mark_line_endings(state, first_line, last_line, token_type) - for line = first_line, last_line - 1 do +local function mark_line_endings(state, token_type) + for line = state.line, state.lexer.line - 1 do state.line_endings[line] = token_type end 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 - parser.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 + local token, token_value, line, offset, error_end_offset = lexer.next_token(state.lexer) + state.token = token + state.token_value = token_value + state.line = line + state.offset = offset + state.end_offset = error_end_offset or (state.lexer.offset - 1) + + if not token then + parser.syntax_error(token_value, state) + end + + if token == "short_comment" then + state.comments[#state.comments + 1] = { + contents = token_value, + line = line, + offset = offset, + end_offset = state.end_offset } - mark_line_endings(state, state.line, state.lexer.line, "comment") + state.line_endings[line] = "comment" + elseif token == "long_comment" then + mark_line_endings(state, "comment") else - if state.token ~= "eof" then - mark_line_endings(state, state.line, state.lexer.line, "string") - state.code_lines[state.line] = true + if token ~= "eof" then + mark_line_endings(state, "string") + state.code_lines[line] = true state.code_lines[state.lexer.line] = true end - break + return 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) + return token == "name" and "identifier" or (token == "eof" and "" or ("'" .. token .. "'")) end -local function parse_error(state, msg, prev_loc, prev_end_column) - local token_repr, end_column +local function parse_error(state, msg, prev_range, token_prefix, message_suffix) + local token_repr 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) + token_repr = lexer.get_quoted_substring_or_line(state.lexer, state.line, state.offset, state.end_offset) + end + + if token_prefix then + token_repr = token_prefix .. " " .. token_repr end - parser.syntax_error(state, end_column, msg .. " near " .. token_repr, prev_loc, prev_end_column) + msg = msg .. " near " .. token_repr + + if message_suffix then + msg = msg .. " " .. message_suffix + end + + parser.syntax_error(msg, state, prev_range) end local function check_token(state, token) @@ -137,17 +114,190 @@ end end -local function check_closing_token(state, opening_token, closing_token, opening_loc) - if state.token ~= closing_token then - local err = "expected " .. token_name(closing_token) +local function copy_range(range) + return { + line = range.line, + offset = range.offset, + end_offset = range.end_offset + } +end + +local new_state +local parse_block +local missing_closing_token_error + +-- Attempt to guess a better location for missing `end` and `until` errors (usually they uselessly point to eof). +-- Guessed error token should be selected in such a way that inserting previously missing closing token +-- in front of it should fix the error or at least move its opening token forward. +-- The idea is to track the stack of opening tokens and their indentations. +-- For the first statement or closing token with the same or smaller indentation than the opening token +-- on the top of the stack: +-- * If it has the same indentation but is not the appropriate closing token for the opening one, pick it +-- as the guessed error location. +-- * If it has a lower indentation level, pick it as the guessed error location even it closes the opening token. +-- Example: +-- local function f() +-- +-- +-- if cond then <- `if` is the guessed opening token +-- +-- +-- <- first token on this line is the guessed error location +-- end +-- Another one: +-- local function g() +-- +-- +-- if cond then <- `if` is the guessed opening token +-- +-- +-- end <- `end` is the guessed error location + +local opening_token_to_closing = { + ["("] = ")", + ["["] = "]", + ["{"] = "}", + ["do"] = "end", + ["if"] = "end", + ["else"] = "end", + ["elseif"] = "end", + ["while"] = "end", + ["repeat"] = "until", + ["for"] = "end", + ["function"] = "end" +} + +local function get_indentation(state, line) + local ws_start, ws_end = state.lexer.src:find("^[ \t\v\f]*", state.lexer.line_offsets[line]) + return ws_end - ws_start +end + +local UnpairedTokenGuesser = utils.class() + +function UnpairedTokenGuesser:__init(state, error_opening_range, error_closing_token) + self.old_state = state + self.error_offset = state.offset + self.error_opening_range = error_opening_range + self.error_closing_token = error_closing_token + self.opening_tokens_stack = utils.Stack() +end + +function UnpairedTokenGuesser:guess() + -- Need to reinitialize lexer (e.g. to skip shebang again). + self.state = new_state(self.old_state.lexer.src) + self.state.unpaired_token_guesser = self + skip_token(self.state) + parse_block(self.state) + error("No syntax error in second parse", 0) +end + +function UnpairedTokenGuesser:on_block_start(opening_token_range, opening_token) + local token_wrapper = copy_range(opening_token_range) + token_wrapper.token = opening_token + token_wrapper.closing_token = opening_token_to_closing[opening_token] + token_wrapper.eligible = token_wrapper.closing_token == self.error_closing_token + token_wrapper.indentation = get_indentation(self.state, opening_token_range.line) + self.opening_tokens_stack:push(token_wrapper) +end + +function UnpairedTokenGuesser:set_guessed() + -- Keep the first detected location. + if self.guessed then + return + end + + self.guessed = self.opening_tokens_stack.top + self.guessed.error_token = self.state.token + self.guessed.error_range = copy_range(self.state) +end + +function UnpairedTokenGuesser:check_token() + local top = self.opening_tokens_stack.top + + if top and top.eligible and self.state.line > top.line then + local token_indentation = get_indentation(self.state, self.state.line) + + if token_indentation < top.indentation then + self:set_guessed() + elseif token_indentation == top.indentation then + local token = self.state.token + + if token ~= top.closing_token and + ((top.token ~= "if" and top.token ~= "elseif") or (token ~= "elseif" and token ~= "else")) then + self:set_guessed() + end + end + end + + if self.state.offset == self.error_offset then + if self.guessed and self.guessed.error_range.offset ~= self.state.offset then + self.state.line = self.guessed.error_range.line + self.state.offset = self.guessed.error_range.offset + self.state.end_offset = self.guessed.error_range.end_offset + self.state.token = self.guessed.error_token + missing_closing_token_error(self.state, self.guessed, self.guessed.token, self.guessed.closing_token, true) + end + end +end + +function UnpairedTokenGuesser:on_block_end() + self:check_token() + self.opening_tokens_stack:pop() + + if not self.opening_tokens_stack.top then + -- Inserting an end token into a balanced sequence of tokens adds an error earlier than original one. + self.guessed = nil + end +end + +function UnpairedTokenGuesser:on_statement() + self:check_token() +end + +function missing_closing_token_error(state, opening_range, opening_token, closing_token, is_guess) + local msg = "expected " .. token_name(closing_token) - if opening_loc.line ~= state.line then - err = err .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_loc.line) .. ")" + if opening_range and opening_range.line ~= state.line then + msg = msg .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_range.line) .. ")" + end + + local token_prefix + local message_suffix + + if is_guess then + if state.token == closing_token then + -- "expected 'end' near 'end'" seems confusing. + token_prefix = "less indented" end - parse_error(state, err, opening_loc, opening_loc.column + #opening_token - 1) + message_suffix = "(indentation-based guess)" end + parse_error(state, msg, opening_range, token_prefix, message_suffix) +end + +local function check_closing_token(state, opening_range, opening_token) + local closing_token = opening_token_to_closing[opening_token] or "eof" + + if state.token == closing_token then + return + end + + if (opening_token == "if" or opening_token == "elseif") and (state.token == "else" or state.token == "elseif") then + return + end + + if closing_token == "end" or closing_token == "until" then + if not state.unpaired_token_guesser then + UnpairedTokenGuesser(state, opening_range, closing_token):guess() + end + end + + missing_closing_token_error(state, opening_range, opening_token, closing_token) +end + +local function check_and_skip_closing_token(state, opening_range, opening_token) + check_closing_token(state, opening_range, opening_token) skip_token(state) end @@ -156,41 +306,47 @@ 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] +local function new_outer_node(range, tag, node) + node = node or {} + node.line = range.line + node.offset = range.offset + node.end_offset = range.end_offset + node.tag = tag + return node +end - 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 +local function new_inner_node(start_range, end_range, tag, node) + node = node or {} + node.line = start_range.line + node.offset = start_range.offset + node.end_offset = end_range.end_offset + node.tag = tag + return node end -local parse_block, parse_expression +local parse_expression -local function parse_expression_list(state) - local list = {} - local is_inside_parentheses +local function parse_expression_list(state, list) + list = list or {} repeat - list[#list+1], is_inside_parentheses = parse_expression(state) + list[#list + 1] = 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") + local ast_node = new_outer_node(state, tag or "Id") ast_node[1] = check_name(state) - skip_token(state) -- Skip name. + -- Skip name. + skip_token(state) return ast_node end local function atom(tag) return function(state) - local ast_node = new_ast_node(state, tag) + local ast_node = new_outer_node(state, tag) ast_node[1] = state.token_value skip_token(state) return ast_node @@ -207,78 +363,73 @@ simple_expressions["..."] = atom("Dots") simple_expressions["{"] = function(state) - local ast_node = new_ast_node(state, "Table") - local start_location = location(state) + local start_range = copy_range(state) + local ast_node = {} skip_token(state) - local is_inside_parentheses = false repeat if state.token == "}" then break - else - local lhs, rhs - local item_location = location(state) - local first_key_token + end - if state.token == "name" then - local name = state.token_value - skip_token(state) -- Skip name. + local key_node, value_node + local first_token_range = copy_range(state) - if test_and_skip_token(state, "=") then - -- `name` = `expr`. - first_key_token = name - 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, nil, true) - end - elseif state.token == "[" then - -- [ `expr` ] = `expr`. - item_location = location(state) - first_key_token = "[" - skip_token(state) - lhs = parse_expression(state) - check_closing_token(state, "[", "]", item_location) - check_and_skip_token(state, "=") - rhs = parse_expression(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`. + key_node = new_outer_node(first_token_range, "String", {name}) + value_node = parse_expression(state) else - -- Expression in array part. - rhs, is_inside_parentheses = parse_expression(state, nil, true) + -- `name` is beginning of an expression in array part. + -- Backtrack lexer to before name. + state.lexer.line = first_token_range.line + state.lexer.offset = first_token_range.offset + skip_token(state) -- Load name again. + value_node = parse_expression(state) end + elseif state.token == "[" then + -- [ `expr` ] = `expr`. + skip_token(state) + key_node = parse_expression(state) + check_and_skip_closing_token(state, first_token_range, "[") + check_and_skip_token(state, "=") + value_node = parse_expression(state) + else + -- Expression in array part. + value_node = parse_expression(state) + end - if lhs then - -- Pair. - ast_node[#ast_node+1] = init_ast_node({lhs, rhs, first_token = first_key_token}, item_location, "Pair") - else - -- Array part item. - ast_node[#ast_node+1] = rhs - end + if key_node then + -- Pair. + ast_node[#ast_node + 1] = new_inner_node(first_token_range, value_node, "Pair", {key_node, value_node}) + else + -- Array part item. + ast_node[#ast_node + 1] = value_node end until not (test_and_skip_token(state, ",") or test_and_skip_token(state, ";")) - check_closing_token(state, "{", "}", start_location) - opt_add_parens(ast_node, is_inside_parentheses) + new_inner_node(start_range, state, "Table", ast_node) + check_and_skip_closing_token(state, start_range, "{") return ast_node end -- Parses argument list and the statements. -local function parse_function(state, func_location) - local paren_location = location(state) +local function parse_function(state, function_range) + local paren_range = copy_range(state) check_and_skip_token(state, "(") local args = {} - if state.token ~= ")" then -- Are there arguments? + -- Are there arguments? + if state.token ~= ")" then repeat if state.token == "name" then - args[#args+1] = parse_id(state) + args[#args + 1] = parse_id(state) elseif state.token == "..." then - args[#args+1] = simple_expressions["..."](state) + args[#args + 1] = simple_expressions["..."](state) break else parse_error(state, "expected argument") @@ -286,84 +437,97 @@ until not test_and_skip_token(state, ",") end - check_closing_token(state, "(", ")", paren_location) - local body = parse_block(state) - local end_location = location(state) - check_closing_token(state, "function", "end", func_location) - return init_ast_node({args, body, end_location = end_location}, func_location, "Function") + check_and_skip_closing_token(state, paren_range, "(") + local body = parse_block(state, function_range, "function") + local end_range = copy_range(state) + -- Skip "function". + skip_token(state) + return new_inner_node(function_range, end_range, "Function", {args, body, end_range = end_range}) end simple_expressions["function"] = function(state) - local function_location = location(state) - skip_token(state) -- Skip "function". - return parse_function(state, function_location) + local function_range = copy_range(state) + -- Skip "function". + skip_token(state) + return parse_function(state, function_range) end -local calls = {} +-- A call handler parses arguments of a call with given base node that determines resulting node start location, +-- given tag, and array to which the arguments should be appended. +local call_handlers = {} + +call_handlers["("] = function(state, base_node, tag, node) + local paren_range = copy_range(state) + -- Skip "(". + skip_token(state) + + if state.token ~= ")" then + parse_expression_list(state, node) + end -calls["("] = function(state) - local paren_location = location(state) - skip_token(state) -- Skip "(". - local args = (state.token == ")") and {} or parse_expression_list(state) - check_closing_token(state, "(", ")", paren_location) - return args + new_inner_node(base_node, state, tag, node) + check_and_skip_closing_token(state, paren_range, "(") + return node end -calls["{"] = function(state) - return {simple_expressions[state.token](state)} +call_handlers["{"] = function(state, base_node, tag, node) + local arg_node = simple_expressions[state.token](state) + node[#node + 1] = arg_node + return new_inner_node(base_node, arg_node, tag, node) end -calls.string = calls["{"] +call_handlers.string = call_handlers["{"] -local suffixes = {} +local suffix_handlers = {} -suffixes["."] = function(state, lhs) - skip_token(state) -- Skip ".". - local rhs = parse_id(state, "String") - return init_ast_node({lhs, rhs}, lhs.location, "Index") +suffix_handlers["."] = function(state, base_node) + -- Skip ".". + skip_token(state) + local index_node = parse_id(state, "String") + return new_inner_node(base_node, index_node, "Index", {base_node, index_node}) end -suffixes["["] = function(state, lhs) - local bracket_location = location(state) - skip_token(state) -- Skip "[". - local rhs = parse_expression(state) - check_closing_token(state, "[", "]", bracket_location) - return init_ast_node({lhs, rhs}, lhs.location, "Index") +suffix_handlers["["] = function(state, base_node) + local bracket_range = copy_range(state) + -- Skip "[". + skip_token(state) + local index_node = parse_expression(state) + local ast_node = new_inner_node(base_node, state, "Index", {base_node, index_node}) + check_and_skip_closing_token(state, bracket_range, "[") + return ast_node end -suffixes[":"] = function(state, lhs) - skip_token(state) -- Skip ":". +suffix_handlers[":"] = function(state, base_node) + -- Skip ":". + skip_token(state) 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") + local call_handler = call_handlers[state.token] + + if not call_handler then + parse_error(state, "expected method arguments") + end + + return call_handler(state, base_node, "Invoke", {base_node, method_name}) end -suffixes["("] = function(state, lhs) - local args = calls[state.token](state) - table.insert(args, 1, lhs) - return init_ast_node(args, lhs.location, "Call") +suffix_handlers["("] = function(state, base_node) + return call_handlers[state.token](state, base_node, "Call", {base_node}) end -suffixes["{"] = suffixes["("] -suffixes.string = suffixes["("] +suffix_handlers["{"] = suffix_handlers["("] +suffix_handlers.string = suffix_handlers["("] --- Additionally returns whether the expression is inside parens and the first non-paren token. local function parse_simple_expression(state, kind, no_literals) - local expression, first_token - local in_parens = false + local expression if state.token == "(" then - in_parens = true - local paren_location = location(state) + local paren_range = copy_range(state) skip_token(state) - local _ - expression, _, first_token = parse_expression(state) - check_closing_token(state, "(", ")", paren_location) + local inner_expression = parse_expression(state) + expression = new_inner_node(paren_range, state, "Paren", {inner_expression}) + check_and_skip_closing_token(state, paren_range, "(") elseif state.token == "name" then expression = parse_id(state) - first_token = expression[1] else local literal_handler = simple_expressions[state.token] @@ -371,25 +535,23 @@ parse_error(state, "expected " .. (kind or "expression")) end - first_token = token_body_or_line(state) - return literal_handler(state), false, first_token + return literal_handler(state) end while true do - local suffix_handler = suffixes[state.token] + local suffix_handler = suffix_handlers[state.token] if suffix_handler then - in_parens = false expression = suffix_handler(state, expression) else - return expression, in_parens, first_token + return expression end end end local unary_operators = { ["not"] = "not", - ["-"] = "unm", -- Not mentioned in Metalua documentation. + ["-"] = "unm", ["~"] = "bnot", ["#"] = "len" } @@ -438,21 +600,18 @@ ["and"] = 2, ["or"] = 1 } --- Additionally returns whether subexpression is inside parentheses, and its first non-paren token. local function parse_subexpression(state, limit, kind) local expression - local first_token - local in_parens = false local unary_operator = unary_operators[state.token] if unary_operator then - first_token = state.token - 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") + local operator_range = copy_range(state) + -- Skip operator. + skip_token(state) + local operand = parse_subexpression(state, unary_priority) + expression = new_inner_node(operator_range, operand, "Op", {unary_operator, operand}) else - expression, in_parens, first_token = parse_simple_expression(state, kind) + expression = parse_simple_expression(state, kind) end -- Expand while operators have priorities higher than `limit`. @@ -463,71 +622,99 @@ break end - in_parens = false - skip_token(state) -- Skip operator. + -- Skip operator. + skip_token(state) -- 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") + expression = new_inner_node(expression, subexpression, "Op", {binary_operator, expression, subexpression}) end - return expression, in_parens, first_token + return expression end --- Additionally returns whether expression is inside parentheses and the first non-paren token. -function parse_expression(state, kind, save_first_token) - local expression, in_parens, first_token = parse_subexpression(state, 0, kind) - expression.first_token = save_first_token and first_token - return expression, in_parens, first_token +function parse_expression(state, kind) + return parse_subexpression(state, 0, kind) end local statements = {} -statements["if"] = function(state, loc) - local start_location, start_token - local next_location, next_token = loc, "if" - local ast_node = init_ast_node({}, loc, "If") +statements["if"] = function(state) + local start_range = copy_range(state) + -- Skip "if". + skip_token(state) + local ast_node = {} - repeat - ast_node[#ast_node+1] = parse_expression(state, "condition", true) - local branch_location = location(state) + -- The loop is entered after skipping "if" or "elseif". + -- Block start token info is set to the last skipped "if", "elseif", or "else" token. + local block_start_token = "if" + local block_start_range = start_range + + while true do + ast_node[#ast_node + 1] = parse_expression(state, "condition") + -- Add range of the "then" token to the block statement array. + local branch_range = copy_range(state) check_and_skip_token(state, "then") - ast_node[#ast_node+1] = parse_block(state, branch_location) - start_location, start_token = next_location, next_token - next_location, next_token = location(state), state.token - until not test_and_skip_token(state, "elseif") - - if state.token == "else" then - start_location, start_token = next_location, next_token - local branch_location = location(state) - skip_token(state) - ast_node[#ast_node+1] = parse_block(state, branch_location) + ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range) + + if state.token == "else" then + branch_range = copy_range(state) + block_start_token = "else" + block_start_range = branch_range + skip_token(state) + ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range) + break + elseif state.token == "elseif" then + block_start_token = "elseif" + block_start_range = copy_range(state) + skip_token(state) + else + break + end end - check_closing_token(state, start_token, "end", start_location) + new_inner_node(start_range, state, "If", ast_node) + -- Skip "end". + skip_token(state) return ast_node end -statements["while"] = function(state, loc) +statements["while"] = function(state) + local start_range = copy_range(state) + -- Skip "while". + skip_token(state) local condition = parse_expression(state, "condition") check_and_skip_token(state, "do") - local block = parse_block(state) - check_closing_token(state, "while", "end", loc) - return init_ast_node({condition, block}, loc, "While") + local block = parse_block(state, start_range, "while") + local ast_node = new_inner_node(start_range, state, "While", {condition, block}) + -- Skip "end". + skip_token(state) + return ast_node end -statements["do"] = function(state, loc) - local ast_node = init_ast_node(parse_block(state), loc, "Do") - check_closing_token(state, "do", "end", loc) +statements["do"] = function(state) + local start_range = copy_range(state) + -- Skip "do". + skip_token(state) + local block = parse_block(state, start_range, "do") + local ast_node = new_inner_node(start_range, state, "Do", block) + -- Skip "end". + skip_token(state) return ast_node end -statements["for"] = function(state, loc) - local ast_node = init_ast_node({}, loc) -- Will set ast_node.tag later. +statements["for"] = function(state) + local start_range = copy_range(state) + -- Skip "for". + skip_token(state) + + local ast_node = {} + local tag local first_var = parse_id(state) if state.token == "=" then -- Numeric "for" loop. - ast_node.tag = "Fornum" + tag = "Fornum" + -- Skip "=". skip_token(state) ast_node[1] = first_var ast_node[2] = parse_expression(state) @@ -539,178 +726,191 @@ end check_and_skip_token(state, "do") - ast_node[#ast_node+1] = parse_block(state) + ast_node[#ast_node + 1] = parse_block(state, start_range, "for") elseif state.token == "," or state.token == "in" then -- Generic "for" loop. - ast_node.tag = "Forin" + tag = "Forin" local iter_vars = {first_var} while test_and_skip_token(state, ",") do - iter_vars[#iter_vars+1] = parse_id(state) + 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) + ast_node[3] = parse_block(state, start_range, "for") else parse_error(state, "expected '=', ',' or 'in'") end - check_closing_token(state, "for", "end", loc) + new_inner_node(start_range, state, tag, ast_node) + -- Skip "end". + skip_token(state) return ast_node end -statements["repeat"] = function(state, loc) - local block = parse_block(state) - check_closing_token(state, "repeat", "until", loc) - local condition = parse_expression(state, "condition", true) - return init_ast_node({block, condition}, loc, "Repeat") +statements["repeat"] = function(state) + local start_range = copy_range(state) + -- Skip "repeat". + skip_token(state) + local block = parse_block(state, start_range, "repeat") + -- Skip "until". + skip_token(state) + local condition = parse_expression(state, "condition") + return new_inner_node(start_range, condition, "Repeat", {block, condition}) end -statements["function"] = function(state, loc) - local lhs_location = location(state) +statements["function"] = function(state) + local start_range = copy_range(state) + -- Skip "function". + skip_token(state) local lhs = parse_id(state) - local self_location + local implicit_self_range - 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") + while (not implicit_self_range) and (state.token == "." or state.token == ":") do + implicit_self_range = (state.token == ":") and copy_range(state) + -- Skip "." or ":". + skip_token(state) + local index_node = parse_id(state, "String") + lhs = new_inner_node(lhs, index_node, "Index", {lhs, index_node}) end - local function_node = parse_function(state, loc) + local function_node = parse_function(state, start_range) - if self_location then + if implicit_self_range then -- Insert implicit "self" argument. - local self_arg = init_ast_node({"self", implicit = true}, self_location, "Id") + local self_arg = new_outer_node(implicit_self_range, "Id", {"self", implicit = true}) table.insert(function_node[1], 1, self_arg) end - return init_ast_node({{lhs}, {function_node}}, loc, "Set") + return new_inner_node(start_range, function_node, "Set", {{lhs}, {function_node}}) end -statements["local"] = function(state, loc) +statements["local"] = function(state) + local start_range = copy_range(state) + -- Skip "local". + skip_token(state) + if state.token == "function" then - -- Localrec - local function_location = location(state) - skip_token(state) -- Skip "function". + -- Local function. + local function_range = copy_range(state) + -- Skip "function". + skip_token(state) 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") + local function_node = parse_function(state, function_range) + return new_inner_node(start_range, function_node, "Localrec", {{var}, {function_node}}) end + -- Local definition, potentially with assignment. local lhs = {} local rhs repeat - lhs[#lhs+1] = parse_id(state) + 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") + return new_inner_node(start_range, rhs and rhs[#rhs] or lhs[#lhs], "Local", {lhs, rhs}) end -statements["::"] = function(state, loc) - local end_column = loc.column + 1 +statements["::"] = function(state) + local start_range = copy_range(state) + -- Skip "::". + skip_token(state) 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 - + -- Skip label name. + skip_token(state) + local ast_node = new_inner_node(start_range, state, "Label", {name}) check_and_skip_token(state, "::") - return init_ast_node({name, end_column = end_column}, loc, "Label") + return ast_node end -local closing_tokens = utils.array_to_set({ - "end", "eof", "else", "elseif", "until"}) +local closing_tokens = utils.array_to_set({"end", "eof", "else", "elseif", "until"}) + +statements["return"] = function(state) + local start_range = copy_range(state) + -- Skip "return". + skip_token(state) -statements["return"] = function(state, loc) if closing_tokens[state.token] or state.token == ";" then -- No return values. - return init_ast_node({}, loc, "Return") + return new_outer_node(start_range, "Return") else - return init_ast_node(parse_expression_list(state), loc, "Return") + local returns = parse_expression_list(state) + return new_inner_node(start_range, returns[#returns], "Return", returns) end end -statements["break"] = function(_, loc) - return init_ast_node({}, loc, "Break") +statements["break"] = function(state) + local ast_node = new_outer_node(state, "Break") + -- Skip "break". + skip_token(state) + return ast_node end -statements["goto"] = function(state, loc) +statements["goto"] = function(state) + local start_range = copy_range(state) + -- Skip "goto". + skip_token(state) local name = check_name(state) - skip_token(state) -- Skip label name. - return init_ast_node({name}, loc, "Goto") + local ast_node = new_outer_node(start_range, "Goto", {name}) + -- Skip label name. + skip_token(state) + return ast_node end -local function parse_expression_statement(state, loc) +local function parse_expression_statement(state) local lhs + local start_range = copy_range(state) + -- Handle lhs of an assignment or a single expression. repeat - local first_loc = lhs and location(state) or loc + local item_start_range = lhs and copy_range(state) or start_range local expected = lhs and "identifier or field" or "statement" - local primary_expression, in_parens = parse_simple_expression(state, expected, true) + local primary_expression = parse_simple_expression(state, expected, true) - if in_parens then - -- (expr) is invalid. - parser.syntax_error(first_loc, first_loc.column, "expected " .. expected .. " near '('") + if primary_expression.tag == "Paren" then + -- (expr) in lhs is invalid. + parser.syntax_error("expected " .. expected .. " near '('", item_start_range) 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. + -- The is an assingment, and a call is not valid in lhs. parse_error(state, "expected call or indexing") else - -- It is a call. - primary_expression.location = loc + -- This is a call. return primary_expression end end -- This is an assignment. lhs = lhs or {} - lhs[#lhs+1] = primary_expression + 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") + return new_inner_node(start_range, rhs[#rhs], "Set", {lhs, rhs}) end local function parse_statement(state) - local loc = location(state) - local statement_parser = statements[state.token] + return (statements[state.token] or parse_expression_statement)(state) +end - if statement_parser then - skip_token(state) - return statement_parser(state, loc) - else - return parse_expression_statement(state, loc) +function parse_block(state, opening_token_range, opening_token, block) + local unpaired_token_guesser = state.unpaired_token_guesser + + if unpaired_token_guesser and opening_token then + unpaired_token_guesser:on_block_start(opening_token_range, opening_token) end -end -function parse_block(state, loc) - local block = {location = loc} + block = block or {} local after_statement = false while not closing_tokens[state.token] do @@ -718,47 +918,67 @@ if first_token == ";" then if not after_statement then - table.insert(state.hanging_semicolons, location(state)) + table.insert(state.hanging_semicolons, copy_range(state)) end + -- Skip ";". skip_token(state) - -- Do not allow several semicolons in a row, even if the first one is valid. + -- Further semicolons are considered hanging. after_statement = false else - first_token = state.token_value or first_token + if unpaired_token_guesser then + unpaired_token_guesser:on_statement() + end + local statement = parse_statement(state) after_statement = true - statement.first_token = first_token - block[#block+1] = statement + block[#block + 1] = statement - if first_token == "return" then + if statement.tag == "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 + break end end end + if unpaired_token_guesser and opening_token then + unpaired_token_guesser:on_block_end() + end + + check_closing_token(state, opening_token_range, opening_token) + return block end --- Parses source string. --- Returns AST (in almost MetaLua format), array of comments - tables {comment = string, location = location}, +function new_state(src, line_offsets, line_lengths) + return { + lexer = lexer.new_state(src, line_offsets, line_lengths), + -- Set of line numbers containing code. + code_lines = {}, + -- Maps line numbers to "comment", "string", or nil based on whether the line ending is within a token + line_endings = {}, + -- Array of {contents = string} with range info. + comments = {}, + -- Array of ranges of semicolons not following a statement. + hanging_semicolons = {} + } +end + +-- Parses source characters. +-- Returns AST (in almost MetaLua format), array of comments - tables {contents = string} with range info, -- set of line numbers containing code, map of types of tokens wrapping line endings (nil, "string", or "comment"), --- and array of locations of empty statements (semicolons). --- On error throws an instance of parser.SyntaxError: a table with required fields `line`, `column`, --- `end_column`, and `msg`, and also optional fields `prev_line`, `prev_column`, and `prev_end_column` if the error --- refers to some other location. -function parser.parse(src) - local state = new_state(src) +-- array of ranges of hanging semicolons (not after statements), array of line start offsets, array of line lengths. +-- The last two tables can be passed as arguments to be filled. +-- On error throws an instance of parser.SyntaxError: table {msg = msg, prev_range = prev_range?} with range info, +-- prev_range may refer to some extra relevant location. +function parser.parse(src, line_offsets, line_lengths) + local state = new_state(src, line_offsets, line_lengths) skip_token(state) local ast = parse_block(state) - check_token(state, "eof") - return ast, state.comments, state.code_lines, state.line_endings, state.hanging_semicolons + return ast, state.comments, state.code_lines, state.line_endings, state.hanging_semicolons, + state.lexer.line_offsets, state.lexer.line_lengths end return parser diff -Nru luacheck-0.22.0/src/luacheck/profiler.lua luacheck-0.23.0/src/luacheck/profiler.lua --- luacheck-0.22.0/src/luacheck/profiler.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/profiler.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,146 @@ +-- Require luasocket only when needed. +local socket + +local profiler = {} + +local metrics = { + {name = "Wall", get = function() return socket.gettime() end}, + {name = "CPU", get = os.clock}, + {name = "Memory", get = function() return collectgarbage("count") end} +} + +local functions = { + {name = "load", module = "cache"}, + {name = "update", module = "cache"}, + {name = "decode", module = "decoder"}, + {name = "parse", module = "parser"}, + {name = "run", module = "stages.unwrap_parens"}, + {name = "run", module = "stages.parse_inline_options"}, + {name = "run", module = "stages.linearize"}, + {name = "run", module = "stages.name_functions"}, + {name = "run", module = "stages.resolve_locals"}, + {name = "run", module = "stages.detect_bad_whitespace"}, + {name = "run", module = "stages.detect_cyclomatic_complexity"}, + {name = "run", module = "stages.detect_empty_blocks"}, + {name = "run", module = "stages.detect_empty_statements"}, + {name = "run", module = "stages.detect_globals"}, + {name = "run", module = "stages.detect_reversed_fornum_loops"}, + {name = "run", module = "stages.detect_unbalanced_assignments"}, + {name = "run", module = "stages.detect_uninit_accesses"}, + {name = "run", module = "stages.detect_unreachable_code"}, + {name = "run", module = "stages.detect_unused_fields"}, + {name = "run", module = "stages.detect_unused_locals"}, + {name = "filter", module = "filter"}, + {name = "normalize", module = "options"} +} + +local stats = {} +local start_values = {} + +local function start_phase(name) + for _, metric in ipairs(metrics) do + start_values[metric][name] = metric.get() + end +end + +local function stop_phase(name) + for _, metric in ipairs(metrics) do + local increment = metric.get() - start_values[metric][name] + stats[metric][name] = (stats[metric][name] or 0) + increment + end +end + +local phase_stack = {} + +local function push_phase(name) + local prev_name = phase_stack[#phase_stack] + + if prev_name then + stop_phase(prev_name) + end + + table.insert(phase_stack, name) + start_phase(name) +end + +local function pop_phase(name) + assert(name == table.remove(phase_stack)) + stop_phase(name) + local prev_name = phase_stack[#phase_stack] + + if prev_name then + start_phase(prev_name) + end +end + +local function continue_wrapper(name, ...) + pop_phase(name) + return ... +end + +local function wrap(fn, name) + return function(...) + push_phase(name) + return continue_wrapper(name, fn(...)) + end +end + +local function patch(fn) + local mod = require("luacheck." .. fn.module) + local orig = mod[fn.name] + local new = wrap(orig, fn.module .. "." .. fn.name) + mod[fn.name] = new +end + +function profiler.init() + socket = require "socket" + collectgarbage("stop") + + for _, metric in ipairs(metrics) do + stats[metric] = {} + start_values[metric] = {} + end + + for _, fn in ipairs(functions) do + patch(fn) + end + + push_phase("other") +end + +function profiler.report() + pop_phase("other") + + for _, metric in ipairs(metrics) do + local names = {} + local total = 0 + + for name, value in pairs(stats[metric]) do + table.insert(names, name) + total = total + value + end + + table.sort(names, function(name1, name2) + local stats1 = stats[metric][name1] + local stats2 = stats[metric][name2] + + if stats1 ~= stats2 then + return stats1 > stats2 + else + return name1 < name2 + end + end) + + print(metric.name) + print() + + for _, name in ipairs(names) do + print(("%s - %f (%f%%)"):format(name, stats[metric][name], stats[metric][name] / total * 100)) + end + + print(("Total - %f"):format(total)) + print() + end +end + +return profiler diff -Nru luacheck-0.22.0/src/luacheck/resolve_locals.lua luacheck-0.23.0/src/luacheck/resolve_locals.lua --- luacheck-0.22.0/src/luacheck/resolve_locals.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/resolve_locals.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,219 +0,0 @@ -local core_utils = require "luacheck.core_utils" - --- The main part of analysis is connecting assignments to locals or upvalues --- with accesses that may use the assigned value. --- Accesses and assignments are split into two groups based on whether they happen --- in the closure that defines subject local variable (main assignment, main access) --- or in some nested closure (closure assignment, closure access). --- To avoid false positives, it's assumed that a closure may be called at any point --- starting from expression that creates it. --- Additionally, all operations on upvalues are considered in bulk, as in, --- when a closure is called, it's assumed that any subset of its upvalue assignments --- and accesses may happen, in any order. - --- Assignments and accesses are connected based on whether they can reach each other. --- A main assignment is connected with a main access when the assignment can reach the access. --- A main assignment is connected with a closure access when the assignment can reach the closure creation --- or the closure creation can reach the assignment. --- A closure assignment is connected with a main access when the closure creation can reach the access. --- A closure assignment is connected with a closure access when either closure creation can reach the other one. - --- To determine what flow graph nodes an assignment or a closure creation can reach, --- they are independently propagated along the graph. --- Closure creation propagation is not bounded. --- Main assignment propagation is bounded by entrance and exit conditions for each reached flow graph node. --- Entrance condition checks that target local variable is still in scope. If entrance condition fails, --- nothing in the node can refer to the variable, and the scope can't be reentered later. --- So, in this case, assignment does not reach the node, and propagation does not continue. --- Exit condition checks that target local variable is not overwritten by an assignment in the node. --- If it fails, the assignment still reaches the node (because all accesses in a node are evaluated before any --- assignments take effect), but propagation does not continue. - -local function register_value(values_per_var, var, value) - if not values_per_var[var] then - values_per_var[var] = {} - end - - table.insert(values_per_var[var], value) -end - --- Called when assignment of `value` is connected to an access. --- `item` contains the access, and `line` contains the item. -local function add_resolution(line, item, var, value, is_mutation) - register_value(item.used_values, var, value) - value[is_mutation and "mutated" or "used"] = true - value.using_lines[line] = true - - if value.secondaries then - value.secondaries.used = true - end -end - --- Connects accesses in given items array with an assignment of `value`. --- `items` may be `nil` instead of empty. -local function add_resolutions(line, items, var, value, is_mutation) - if not items then - return - end - - for _, item in ipairs(items) do - add_resolution(line, item, var, value, is_mutation) - end -end - --- Connects all accesses (and mutations) in `access_line` with corresponding --- assignments in `set_line`. -local function cross_resolve_closures(access_line, set_line) - for var, setting_items in pairs(set_line.set_upvalues) do - for _, setting_item in ipairs(setting_items) do - add_resolutions(access_line, access_line.accessed_upvalues[var], - var, setting_item.set_variables[var]) - add_resolutions(access_line, access_line.mutated_upvalues[var], - var, setting_item.set_variables[var], true) - end - end -end - -local function in_scope(var, index) - return (var.scope_start <= index) and (index <= var.scope_end) -end - --- Called when main assignment propagation reaches a line item. -local function main_assignment_propagation_callback(line, index, item, var, value) - -- Check entrance condition. - if not in_scope(var, index) then - -- Assignment reaches the end of variable scope, so it can't be dominated by any assignment. - value.overwriting_item = false - return true - end - - -- Assignment reaches this item, apply its effect. - - -- Accesses (and mutations) of the variable can resolve to reaching assignment. - if item.accesses and item.accesses[var] then - add_resolution(line, item, var, value) - end - - if item.mutations and item.mutations[var] then - add_resolution(line, item, var, value, true) - end - - -- Accesses (and mutations) of the variable inside closures created in this item - -- can resolve to reaching assignment. - if item.lines then - for _, created_line in ipairs(item.lines) do - add_resolutions(created_line, created_line.accessed_upvalues[var], var, value) - add_resolutions(created_line, created_line.mutated_upvalues[var], var, value, true) - end - end - - -- Check exit condition. - if item.set_variables and item.set_variables[var] then - if value.overwriting_item ~= false then - if value.overwriting_item and value.overwriting_item ~= item then - value.overwriting_item = false - else - value.overwriting_item = item - end - end - - return true - end -end - --- Connects main assignments with main accesses and closure accesses in reachable closures. --- Additionally, sets `overwriting_item` field of values to an item with an assignment overwriting --- the value, but only if the overwriting is not avoidable (i.e. it's impossible to reach end of function --- from the first assignment without going through the second one). Otherwise value of the field may be --- `false` or `nil`. -local function propagate_main_assignments(line) - for i, item in ipairs(line.items) do - if item.set_variables then - for var, value in pairs(item.set_variables) do - if var.line == line then - -- Assignments are not live at their own item, because assignments take effect only after all accesses - -- are evaluated. Items with assignments can't be jumps, so they have a single following item - -- with incremented index. - core_utils.walk_line(line, {}, i + 1, main_assignment_propagation_callback, var, value) - end - end - end - end -end - --- Called when closure creation propagation reaches a line item. -local function closure_creation_propagation_callback(line, _, item, propagated_line) - if not item then - return true - end - - -- Closure creation reaches this item, apply its effects. - - -- Accesses (and mutations) of upvalues in the propagated closure - -- can resolve to assignments in the item. - if item.set_variables then - for var, value in pairs(item.set_variables) do - add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], var, value) - add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], var, value, true) - end - end - - if item.lines then - for _, created_line in ipairs(item.lines) do - -- Accesses (and mutations) of upvalues in the propagated closure - -- can resolve to assignments in closures created in the item. - cross_resolve_closures(propagated_line, created_line) - - -- Accesses (and mutations) of upvalues in closures created in the item - -- can resolve to assignments in the propagated closure. - cross_resolve_closures(created_line, propagated_line) - end - end - - -- Accesses (and mutations) of locals in the item can resolve - -- to assignments in the propagated closure. - for var, setting_items in pairs(propagated_line.set_upvalues) do - if item.accesses and item.accesses[var] then - for _, setting_item in ipairs(setting_items) do - add_resolution(line, item, var, setting_item.set_variables[var]) - end - end - - if item.mutations and item.mutations[var] then - for _, setting_item in ipairs(setting_items) do - add_resolution(line, item, var, setting_item.set_variables[var], true) - end - end - end -end - --- Connects main assignments with closure accesses in reaching closures. --- Connects closure assignments with main accesses and with closure accesses in reachable closures. --- Connects closure accesses with closure assignments in reachable closures. -local function propagate_closure_creations(line) - for i, item in ipairs(line.items) do - if item.lines then - for _, created_line in ipairs(item.lines) do - -- Closures are live at the item they are created, as they can be called immediately. - core_utils.walk_line(line, {}, i, closure_creation_propagation_callback, created_line) - end - end - end -end - -local function analyze_line(line) - propagate_main_assignments(line) - propagate_closure_creations(line) -end - - --- Finds reaching assignments for all local variable accesses. -local function resolve_locals(chstate) - analyze_line(chstate.main_line) - - for _, nested_line in ipairs(chstate.main_line.lines) do - analyze_line(nested_line) - end -end - -return resolve_locals diff -Nru luacheck-0.22.0/src/luacheck/runner.lua luacheck-0.23.0/src/luacheck/runner.lua --- luacheck-0.22.0/src/luacheck/runner.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/runner.lua 2018-09-18 19:43:27.000000000 +0000 @@ -141,20 +141,15 @@ for _, input in ipairs(inputs) do if input.path then if fs.is_dir(input.path) then - if fs.has_lfs then - local filenames, err_map = fs.extract_files(input.path, dir_pattern) + local filenames, err_map = fs.extract_files(input.path, dir_pattern) - for _, filename in ipairs(filenames) do - local err = err_map[filename] - if err then - add({path = filename, fatal = "I/O", msg = err, filename = input.filename}) - else - add({path = filename, filename = input.filename}) - end + for _, filename in ipairs(filenames) do + local err = err_map[filename] + if err then + add({path = filename, fatal = "I/O", msg = err, filename = input.filename}) + else + add({path = filename, filename = input.filename}) end - else - local err = "LuaFileSystem required to check directories" - add({path = input.path, fatal = "I/O", msg = err, filename = input.filename}) end else add({path = input.path, filename = input.filename}) diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_bad_whitespace.lua luacheck-0.23.0/src/luacheck/stages/detect_bad_whitespace.lua --- luacheck-0.22.0/src/luacheck/stages/detect_bad_whitespace.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_bad_whitespace.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,76 @@ +local stage = {} + +stage.warnings = { + ["611"] = {message_format = "line contains only whitespace", fields = {}}, + ["612"] = {message_format = "line contains trailing whitespace", fields = {}}, + ["613"] = {message_format = "trailing whitespace in a string", fields = {}}, + ["614"] = {message_format = "trailing whitespace in a comment", fields = {}}, + ["621"] = {message_format = "inconsistent indentation (SPACE followed by TAB)", fields = {}} +} + +function stage.run(chstate) + local num_lines = #chstate.line_offsets + + for line_number = 1, num_lines do + local line_offset = chstate.line_offsets[line_number] + local line_length = chstate.line_lengths[line_number] + + if line_length > 0 then + local trailing_ws_pattern + + if line_number == num_lines then + trailing_ws_pattern = "^[^\r\n]-()[ \t\f\v]+()[\r\n]?$" + else + trailing_ws_pattern = "^[^\r\n]-()[ \t\f\v]+()[\r\n]" + end + + local line_start_byte, _, trailing_ws_start_byte, line_end_byte = chstate.source:find( + trailing_ws_pattern, line_offset) + + local trailing_ws_code + + if trailing_ws_start_byte then + + if trailing_ws_start_byte == line_start_byte then + -- Line contains only whitespace (thus never considered "code"). + trailing_ws_code = "611" + elseif not chstate.line_endings[line_number] then + -- Trailing whitespace on code line or after long comment. + trailing_ws_code = "612" + elseif chstate.line_endings[line_number] == "string" then + -- Trailing whitespace embedded in a string literal. + trailing_ws_code = "613" + elseif chstate.line_endings[line_number] == "comment" then + -- Trailing whitespace at the end of a line comment or inside long comment. + trailing_ws_code = "614" + end + + -- The difference between the start and the end of the warning range + -- is the same in bytes and in characters because whitespace characters are ASCII. + -- Can calculate one based on the three others. + local trailing_ws_end_byte = line_end_byte - 1 + local trailing_ws_end_char = line_offset + line_length - 1 + local trailing_ws_start_char = trailing_ws_end_char - (trailing_ws_end_byte - trailing_ws_start_byte) + + chstate:warn(trailing_ws_code, line_number, trailing_ws_start_char, trailing_ws_end_char) + end + + -- Don't look for inconsistent whitespace in pure whitespace lines. + if trailing_ws_code ~= "611" then + local leading_ws_start_byte, leading_ws_end_byte = chstate.source:find( + "^[ \t\f\v]- \t[ \t\f\v]*", line_offset) + + if leading_ws_start_byte then + -- Inconsistent leading whitespace (SPACE followed by TAB). + + -- Calculate warning end in characters using same logic as above. + local leading_ws_start_char = line_offset + local leading_ws_end_char = leading_ws_start_char + (leading_ws_end_byte - leading_ws_start_byte) + chstate:warn("621", line_number, line_offset, leading_ws_end_char) + end + end + end + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_cyclomatic_complexity.lua luacheck-0.23.0/src/luacheck/stages/detect_cyclomatic_complexity.lua --- luacheck-0.22.0/src/luacheck/stages/detect_cyclomatic_complexity.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_cyclomatic_complexity.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,159 @@ +local utils = require "luacheck.utils" + +local stage = {} + +local function cyclomatic_complexity_message_format(warning) + local template = "cyclomatic complexity of %s is too high ({complexity} > {max_complexity})" + + local function_descr + + if warning.function_type == "main_chunk" then + function_descr = "main chunk" + elseif warning.function_name then + function_descr = "{function_type} {function_name!}" + else + function_descr = "function" + end + + return template:format(function_descr) +end + +stage.warnings = { + ["561"] = {message_format = cyclomatic_complexity_message_format, + fields = {"complexity", "function_type", "function_name"}} +} + +local function warn_cyclomatic_complexity(chstate, line, complexity) + if line == chstate.top_line then + chstate:warn("561", 1, 1, 1, { + complexity = complexity, + function_type = "main_chunk" + }) + else + local node = line.node + + chstate:warn_range("561", node, { + complexity = complexity, + function_type = node[1][1] and node[1][1].implicit and "method" or "function", + function_name = node.name + }) + end +end + +local CyclomaticComplexityMetric = utils.class() + +function CyclomaticComplexityMetric:incr_decisions(count) + self.count = self.count + count +end + +function CyclomaticComplexityMetric:calc_expr(node) + if node.tag == "Op" and (node[1] == "and" or node[1] == "or") then + self:incr_decisions(1) + end + + if node.tag ~= "Function" then + self:calc_exprs(node) + end +end + +function CyclomaticComplexityMetric:calc_exprs(exprs) + for _, expr in ipairs(exprs) do + if type(expr) == "table" then + self:calc_expr(expr) + end + end +end + +function CyclomaticComplexityMetric:calc_item_Eval(item) + self:calc_expr(item.node) +end + +function CyclomaticComplexityMetric:calc_item_Local(item) + if item.rhs then + self:calc_exprs(item.rhs) + end +end + +function CyclomaticComplexityMetric:calc_item_Set(item) + self:calc_exprs(item.rhs) +end + +function CyclomaticComplexityMetric:calc_item(item) + local f = self["calc_item_" .. item.tag] + if f then + f(self, item) + end +end + +function CyclomaticComplexityMetric:calc_items(items) + for _, item in ipairs(items) do + self:calc_item(item) + end +end + +-- stmt if: {condition, block; condition, block; ... else_block} +function CyclomaticComplexityMetric:calc_stmt_If(node) + for i = 1, #node - 1, 2 do + self:incr_decisions(1) + self:calc_stmts(node[i+1]) + end + + if #node % 2 == 1 then + self:calc_stmts(node[#node]) + end +end + +-- stmt while: {condition, block} +function CyclomaticComplexityMetric:calc_stmt_While(node) + self:incr_decisions(1) + self:calc_stmts(node[2]) +end + +-- stmt repeat: {block, condition} +function CyclomaticComplexityMetric:calc_stmt_Repeat(node) + self:incr_decisions(1) + self:calc_stmts(node[1]) +end + +-- stmt forin: {iter_vars, expression_list, block} +function CyclomaticComplexityMetric:calc_stmt_Forin(node) + self:incr_decisions(1) + self:calc_stmts(node[3]) +end + +-- stmt fornum: {first_var, expression, expression, expression[optional], block} +function CyclomaticComplexityMetric:calc_stmt_Fornum(node) + self:incr_decisions(1) + self:calc_stmts(node[5] or node[4]) +end + +function CyclomaticComplexityMetric:calc_stmt(node) + local f = self["calc_stmt_" .. node.tag] + if f then + f(self, node) + end +end + +function CyclomaticComplexityMetric:calc_stmts(stmts) + for _, stmt in ipairs(stmts) do + self:calc_stmt(stmt) + end +end + +-- Cyclomatic complexity of a function equals to the number of decision points plus 1. +function CyclomaticComplexityMetric:report(chstate, line) + self.count = 1 + self:calc_stmts(line.node[2]) + self:calc_items(line.items) + warn_cyclomatic_complexity(chstate, line, self.count) +end + +function stage.run(chstate) + local ccmetric = CyclomaticComplexityMetric() + + for _, line in ipairs(chstate.lines) do + ccmetric:report(chstate, line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_empty_blocks.lua luacheck-0.23.0/src/luacheck/stages/detect_empty_blocks.lua --- luacheck-0.22.0/src/luacheck/stages/detect_empty_blocks.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_empty_blocks.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,36 @@ +local core_utils = require "luacheck.core_utils" + +local stage = {} + +stage.warnings = { + ["541"] = {message_format = "empty do..end block", fields = {}}, + ["542"] = {message_format = "empty if branch", fields = {}} +} + +local function check_block(chstate, block, code) + if #block == 0 then + chstate:warn_range(code, block) + end +end + + +local function check_node(chstate, node) + if node.tag == "Do" then + check_block(chstate, node, "541") + return + end + + for index = 2, #node, 2 do + check_block(chstate, node[index], "542") + end + + if #node % 2 == 1 then + check_block(chstate, node[#node], "542") + end +end + +function stage.run(chstate) + core_utils.each_statement(chstate, {"Do", "If"}, check_node) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_empty_statements.lua luacheck-0.23.0/src/luacheck/stages/detect_empty_statements.lua --- luacheck-0.22.0/src/luacheck/stages/detect_empty_statements.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_empty_statements.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,13 @@ +local stage = {} + +stage.warnings = { + ["551"] = {message_format = "empty statement", fields = {}} +} + +function stage.run(chstate) + for _, range in ipairs(chstate.useless_semicolons) do + chstate:warn_range("551", range) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_globals.lua luacheck-0.23.0/src/luacheck/stages/detect_globals.lua --- luacheck-0.22.0/src/luacheck/stages/detect_globals.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_globals.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,252 @@ +local utils = require "luacheck.utils" + +local stage = {} + +local function prefix_if_indirect(message) + return function(warning) + if warning.indirect then + return "indirectly " .. message + else + return message + end + end +end + +local function setting_global_format_message(warning) + -- `module` field is set during filtering. + if warning.module then + return "setting non-module global variable {name!}" + else + return "setting non-standard global variable {name!}" + end +end +local global_warning_fields = {"name", "indexing", "previous_indexing_len", "top", "indirect"} + +stage.warnings = { + ["111"] = {message_format = setting_global_format_message, fields = global_warning_fields}, + ["112"] = {message_format = "mutating non-standard global variable {name!}", fields = global_warning_fields}, + ["113"] = {message_format = "accessing undefined variable {name!}", fields = global_warning_fields}, + -- The following warnings are added during filtering. + ["121"] = {message_format = "setting read-only global variable {name!}", fields = {}}, + ["122"] = {message_format = prefix_if_indirect("setting read-only field {field!} of global {name!}"), fields = {}}, + ["131"] = {message_format = "unused global variable {name!}", fields = {}}, + ["142"] = {message_format = prefix_if_indirect("setting undefined field {field!} of global {name!}"), fields = {}}, + ["143"] = {message_format = prefix_if_indirect("accessing undefined field {field!} of global {name!}"), fields = {}} +} + +local action_codes = { + set = "1", + mutate = "2", + access = "3" +} + +-- `index` describes an indexing, where `index[1]` is a global node +-- and other items describe keys: each one is a string node, "not_string", +-- or "unknown". `node` is literal base node that's indexed. +-- E.g. in `local a = table.a; a.b = "c"` `node` is `a` node of the second +-- statement and `index` describes `table.a.b`. +-- `index.previous_indexing_len` is optional length of prefix of `index` array representing last assignment +-- in the aliasing chain, e.g. `2` in the previous example (because last indexing is `table.a`). +local function warn_global(chstate, node, index, is_lhs, is_top_line) + local global = index[1] + local action = is_lhs and (#index == 1 and "set" or "mutate") or "access" + + local indexing + + if #index > 1 then + indexing = {} + + for i, field in ipairs(index) do + if i > 1 then + if field == "unknown" then + indexing[i - 1] = true + elseif field == "not_string" then + indexing[i - 1] = false + else + indexing[i - 1] = field[1] + end + end + end + end + + chstate:warn_range("11" .. action_codes[action], node, { + name = global[1], + indexing = indexing, + previous_indexing_len = index.previous_indexing_len, + top = is_top_line and action == "set" or nil, + indirect = node ~= global or nil + }) +end + +local function resolved_to_index(resolution) + return resolution ~= "unknown" and resolution ~= "not_string" and resolution.tag ~= "String" +end + +local literal_tags = utils.array_to_set({"Nil", "True", "False", "Number", "String", "Table", "Function"}) + +local deep_resolve -- Forward declaration. + +local function resolve_node(node, item) + if node.tag == "Id" or node.tag == "Index" then + deep_resolve(node, item) + return node.resolution + elseif literal_tags[node.tag] then + return node.tag == "String" and node or "not_string" + else + return "unknown" + end +end + +-- Resolves value of an identifier or index node, tracking through simple +-- assignments like `local foo = bar.baz`. +-- Can be given an `Invoke` node to resolve the method field. +-- Sets `node.resolution` to "unknown", "not_string", `string node`, or +-- {previous_indexing_len = index, global_node, key...}. +-- Each key can be "unknown", "not_string" or `string_node`. +function deep_resolve(node, item) + if node.resolution then + return + end + + -- Common case. + -- Also protects against infinite recursion, if it's even possible. + node.resolution = "unknown" + + local base = node + local base_tag = node.tag == "Id" and "Id" or "Index" + local keys = {} + + while base_tag == "Index" do + table.insert(keys, 1, base[2]) + base = base[1] + base_tag = base.tag + end + + if base_tag ~= "Id" then + return + end + + local var = base.var + local base_resolution + local previous_indexing_len + + if var then + if not item.used_values[var] or #item.used_values[var] ~= 1 then + -- Do not know where the value for the base local came from. + return + end + + local value = item.used_values[var][1] + + if not value.node then + return + end + + base_resolution = resolve_node(value.node, value.item) + + if resolved_to_index(base_resolution) then + previous_indexing_len = #base_resolution + end + else + base_resolution = {base} + end + + if #keys == 0 then + node.resolution = base_resolution + elseif not resolved_to_index(base_resolution) then + -- Indexing something unknown or indexing a literal. + node.resolution = "unknown" + else + local resolution = utils.update({}, base_resolution) + resolution.previous_indexing_len = previous_indexing_len + + for _, key in ipairs(keys) do + local key_resolution = resolve_node(key, item) + + if resolved_to_index(key_resolution) then + key_resolution = "unknown" + end + + table.insert(resolution, key_resolution) + end + + -- Assign resolution only after all the recursive calls. + node.resolution = resolution + end +end + +local function detect_in_node(chstate, item, node, is_top_line, is_lhs) + if node.tag == "Index" or node.tag == "Invoke" or node.tag == "Id" then + if node.tag == "Id" and node.var then + -- Do not warn about assignments to and accesses of local variables + -- that resolve to globals or their fields. + return + end + + deep_resolve(node, item) + local resolution = node.resolution + + -- Still need to recurse into base and key nodes. + -- E.g. don't miss a global in `(global1())[global2()]. + + if node.tag == "Invoke" then + for i = 3, #node do + detect_in_node(chstate, item, node[i], is_top_line) + end + end + + if node.tag ~= "Id" then + repeat + detect_in_node(chstate, item, node[2], is_top_line) + node = node[1] + until node.tag ~= "Index" + + if node.tag ~= "Id" then + detect_in_node(chstate, item, node, is_top_line) + end + end + + if resolved_to_index(resolution) then + warn_global(chstate, node, resolution, is_lhs, is_top_line) + end + elseif node.tag ~= "Function" then + for _, nested_node in ipairs(node) do + if type(nested_node) == "table" then + detect_in_node(chstate, item, nested_node, is_top_line) + end + end + end +end + +local function detect_in_nodes(chstate, item, nodes, is_top_line, is_lhs) + for _, node in ipairs(nodes) do + detect_in_node(chstate, item, node, is_top_line, is_lhs) + end +end + +local function detect_globals_in_line(chstate, line) + local is_top_line = line == chstate.top_line + + for _, item in ipairs(line.items) do + if item.tag == "Eval" then + detect_in_node(chstate, item, item.node, is_top_line) + elseif item.tag == "Local" then + if item.rhs then + detect_in_nodes(chstate, item, item.rhs, is_top_line) + end + elseif item.tag == "Set" then + detect_in_nodes(chstate, item, item.lhs, is_top_line, true) + detect_in_nodes(chstate, item, item.rhs, is_top_line) + end + end +end + +-- Warns about assignments, field accesses, and mutations of global variables, +-- tracing through localizing assignments such as `local t = table`. +function stage.run(chstate) + for _, line in ipairs(chstate.lines) do + detect_globals_in_line(chstate, line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_reversed_fornum_loops.lua luacheck-0.23.0/src/luacheck/stages/detect_reversed_fornum_loops.lua --- luacheck-0.22.0/src/luacheck/stages/detect_reversed_fornum_loops.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_reversed_fornum_loops.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,39 @@ +local core_utils = require "luacheck.core_utils" + +local stage = {} + +stage.warnings = { + ["571"] = {message_format = "numeric for loop goes from #(expr) down to {limit} but loop step is not negative", + fields = {"limit"}} +} + +local function check_fornum(chstate, node) + if node[2].tag ~= "Op" or node[2][1] ~= "len" then + return + end + + local limit, limit_repr = core_utils.eval_const_node(node[3]) + + if not limit or limit > 1 then + return + end + + local step = 1 + + if node[5] then + step = core_utils.eval_const_node(node[4]) + end + + if step and step >= 0 then + chstate:warn_range("571", node, { + limit = limit_repr + }) + end +end + +-- Warns about loops trying to go from `#(expr)` to `1` with positive step. +function stage.run(chstate) + core_utils.each_statement(chstate, {"Fornum"}, check_fornum) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_unbalanced_assignments.lua luacheck-0.23.0/src/luacheck/stages/detect_unbalanced_assignments.lua --- luacheck-0.22.0/src/luacheck/stages/detect_unbalanced_assignments.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_unbalanced_assignments.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,34 @@ +local core_utils = require "luacheck.core_utils" + +local stage = {} + +stage.warnings = { + ["531"] = {message_format = "right side of assignment has more values than left side expects", fields = {}}, + ["532"] = {message_format = "right side of assignment has less values than left side expects", fields = {}} +} + +local function is_unpacking(node) + return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke" +end + +local function check_assignment(chstate, node) + local rhs = node[2] + + if not rhs then + return + end + + local lhs = node[1] + + if #rhs > #lhs then + chstate:warn_range("531", node) + elseif #rhs < #lhs and node.tag == "Set" and not is_unpacking(rhs[#rhs]) then + chstate:warn_range("532", node) + end +end + +function stage.run(chstate) + core_utils.each_statement(chstate, {"Set", "Local"}, check_assignment) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_uninit_accesses.lua luacheck-0.23.0/src/luacheck/stages/detect_uninit_accesses.lua --- luacheck-0.22.0/src/luacheck/stages/detect_uninit_accesses.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_uninit_accesses.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,54 @@ +local stage = {} + +stage.warnings = { + ["321"] = {message_format = "accessing uninitialized variable {name!}", fields = {"name"}}, + ["341"] = {message_format = "mutating uninitialized variable {name!}", fields = {"name"}} +} + +local function detect_uninit_access_in_line(chstate, line) + for _, item in ipairs(line.items) do + for _, action_key in ipairs({"accesses", "mutations"}) do + local code = action_key == "accesses" and "321" or "341" + local item_var_map = item[action_key] + + if item_var_map then + for var, accessing_nodes in pairs(item_var_map) do + -- If there are no values at all reaching this access, not even the empty one, + -- this item (or a closure containing it) is not reachable from variable definition. + -- It will be reported as unreachable code, no need to report uninitalized accesses in it. + if item.used_values[var] then + -- If this variable is has only one, empty value then it's already reported as never set, + -- no need to report each access. + if not (#var.values == 1 and var.values[1].empty) then + local all_possible_values_empty = true + + for _, possible_value in ipairs(item.used_values[var]) do + if not possible_value.empty then + all_possible_values_empty = false + break + end + end + + if all_possible_values_empty then + for _, accessing_node in ipairs(accessing_nodes) do + chstate:warn_range(code, accessing_node, { + name = accessing_node[1] + }) + end + end + end + end + end + end + end + end +end + +-- Warns about accesses and mutations that don't resolve to any values except initial empty one. +function stage.run(chstate) + for _, line in ipairs(chstate.lines) do + detect_uninit_access_in_line(chstate, line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_unreachable_code.lua luacheck-0.23.0/src/luacheck/stages/detect_unreachable_code.lua --- luacheck-0.22.0/src/luacheck/stages/detect_unreachable_code.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_unreachable_code.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,36 @@ +local stage = {} + +stage.warnings = { + ["511"] = {message_format = "unreachable code", fields = {}}, + ["512"] = {message_format = "loop is executed at most once", fields = {}} +} + +local function noop_callback() end + +local function detect_unreachable_code(chstate, line) + local reachable_indexes = {} + + -- Mark all items reachable from the function start. + line:walk(reachable_indexes, 1, noop_callback) + + -- All remaining items are unreachable. + -- However, there is no point in reporting all of them. + -- Only report those that are not reachable from any already reported ones. + for item_index, item in ipairs(line.items) do + if not reachable_indexes[item_index] then + if item.node then + chstate:warn_range(item.loop_end and "512" or "511", item.node) + -- Mark all items reachable from the item just reported. + line:walk(reachable_indexes, item_index, noop_callback) + end + end + end +end + +function stage.run(chstate) + for _, line in ipairs(chstate.lines) do + detect_unreachable_code(chstate, line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_unused_fields.lua luacheck-0.23.0/src/luacheck/stages/detect_unused_fields.lua --- luacheck-0.22.0/src/luacheck/stages/detect_unused_fields.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_unused_fields.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,81 @@ +local core_utils = require "luacheck.core_utils" + +local stage = {} + +local function unused_field_value_message_format(warning) + local target = warning.index and "index" or "field" + return "value assigned to " .. target .. " {field!} is overwritten on line {overwritten_line} before use" +end + +stage.warnings = { + ["314"] = {message_format = unused_field_value_message_format, + fields = {"field", "index", "overwritten_line","overwritten_column", "overwritten_end_column"}} +} + +local function warn_unused_field_value(chstate, node, field_repr, is_index, overwriting_node) + chstate:warn_range("314", node, { + field = field_repr, + index = is_index, + overwritten_line = overwriting_node.line, + overwritten_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.offset), + overwritten_end_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.end_offset) + }) +end + +local function check_table(chstate, node) + local array_index = 1.0 + local key_value_to_node = {} + local key_node_to_repr = {} + local index_key_nodes = {} + + for _, pair in ipairs(node) do + local key_value + local key_repr + local key_node + + if pair.tag == "Pair" then + key_node = pair[1] + key_value, key_repr = core_utils.eval_const_node(key_node) + else + key_node = pair + key_value = array_index + key_repr = tostring(math.floor(key_value)) + array_index = array_index + 1.0 + end + + if key_value then + local prev_key_node = key_value_to_node[key_value] + local prev_key_repr = key_node_to_repr[prev_key_node] + local prev_key_is_index = index_key_nodes[prev_key_node] + + if prev_key_node then + warn_unused_field_value(chstate, prev_key_node, prev_key_repr, prev_key_is_index, key_node) + end + + key_value_to_node[key_value] = key_node + key_node_to_repr[key_node] = key_repr + + if pair.tag ~= "Pair" then + index_key_nodes[key_node] = true + end + end + end +end + +local function check_nodes(chstate, nodes) + for _, node in ipairs(nodes) do + if type(node) == "table" then + if node.tag == "Table" then + check_table(chstate, node) + end + + check_nodes(chstate, node) + end + end +end + +function stage.run(chstate) + check_nodes(chstate, chstate.ast) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/detect_unused_locals.lua luacheck-0.23.0/src/luacheck/stages/detect_unused_locals.lua --- luacheck-0.22.0/src/luacheck/stages/detect_unused_locals.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/detect_unused_locals.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,330 @@ +local utils = require "luacheck.utils" + +local stage = {} + +local function unused_local_message_format(warning) + if warning.func then + if warning.recursive then + return "unused recursive function {name!}" + elseif warning.mutually_recursive then + return "unused mutually recursive function {name!}" + else + return "unused function {name!}" + end + else + return "unused variable {name!}" + end +end + +local function unused_arg_message_format(warning) + if warning.name == "..." then + return "unused variable length argument" + else + return "unused argument {name!}" + end +end + +local function unused_or_overwritten_warning(message_format) + return { + message_format = function(warning) + if warning.overwritten_line then + return message_format .. " is overwritten on line {overwritten_line} before use" + else + return message_format .. " is unused" + end + end, + fields = {"name", "secondary", "overwritten_line", "overwritten_column", "overwritten_end_column"} + } +end + +stage.warnings = { + ["211"] = {message_format = unused_local_message_format, + fields = {"name", "func", "secondary", "useless", "recursive", "mutually_recursive"}}, + ["212"] = {message_format = unused_arg_message_format, fields = {"name", "self"}}, + ["213"] = {message_format = "unused loop variable {name!}", fields = {"name"}}, + ["221"] = {message_format = "variable {name!} is never set", fields = {"name", "secondary"}}, + ["231"] = {message_format = "variable {name!} is never accessed", fields = {"name", "secondary"}}, + ["232"] = {message_format = "argument {name!} is never accessed", fields = {"name"}}, + ["233"] = {message_format = "loop variable {name!} is never accessed", fields = {"name"}}, + ["241"] = {message_format = "variable {name!} is mutated but never accessed", fields = {"name", "secondary"}}, + ["311"] = unused_or_overwritten_warning("value assigned to variable {name!}"), + ["312"] = unused_or_overwritten_warning("value of argument {name!}"), + ["313"] = unused_or_overwritten_warning("value of loop variable {name!}"), + ["331"] = {message_format = "value assigned to variable {name!} is mutated but never accessed", + fields = {"name", "secondary"}} +} + +local function is_secondary(value) + return value.secondaries and value.secondaries.used +end + +local type_codes = { + var = "1", + func = "1", + arg = "2", + loop = "3", + loopi = "3" +} + +local function warn_unused_var(chstate, value, is_useless) + chstate:warn_value("21" .. type_codes[value.var.type], value, { + secondary = is_secondary(value) or nil, + func = value.type == "func" or nil, + self = value.var.self, + useless = value.var.name == "_" and is_useless or nil + }) +end + +local function warn_unaccessed_var(chstate, var, is_mutated) + -- 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 + + chstate:warn_var("2" .. (is_mutated and "4" or "3") .. type_codes[var.type], var, { + secondary = secondary + }) +end + +local function warn_unused_value(chstate, value, overwriting_node) + local warning = chstate:warn_value("3" .. (value.mutated and "3" or "1") .. type_codes[value.type], value, { + secondary = is_secondary(value) or nil + }) + + if overwriting_node then + warning.overwritten_line = overwriting_node.line + warning.overwritten_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.offset) + warning.overwritten_end_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.end_offset) + end +end + +-- Returns `true` if a variable should be reported as a function instead of simply local, +-- `false` otherwise. +-- A variable is considered a function if it has a single assignment and the value is a function, +-- or if there is a forward declaration with a function assignment later. +local function is_function_var(var) + return (#var.values == 1 and var.values[1].type == "func") or ( + #var.values == 2 and var.values[1].empty and var.values[2].type == "func") +end + +local externally_accessible_tags = utils.array_to_set({"Id", "Index", "Call", "Invoke", "Op", "Paren", "Dots"}) + +local function is_externally_accessible(value) + return value.type ~= "var" or (value.node and externally_accessible_tags[value.node.tag]) +end + +local function get_overwriting_lhs_node(item, value) + for _, node in ipairs(item.lhs) do + if node.var == value.var then + return node + end + end +end + +local function get_second_overwriting_lhs_node(item, value) + local after_value_node + + for _, node in ipairs(item.lhs) do + if node.var == value.var then + if after_value_node then + return node + elseif node == value.var_node then + after_value_node = true + end + end + end +end + +local function detect_unused_local(chstate, var) + if is_function_var(var) then + local value = var.values[2] or var.values[1] + + if not value.used then + warn_unused_var(chstate, value) + end + elseif #var.values == 1 then + local value = var.values[1] + + if not value.used then + if value.mutated then + if not is_externally_accessible(value) then + warn_unaccessed_var(chstate, var, true) + end + else + warn_unused_var(chstate, value, value.empty) + end + elseif value.empty then + chstate:warn_var("221", var) + end + elseif not var.accessed and not var.mutated then + warn_unaccessed_var(chstate, var) + else + local no_values_externally_accessible = true + + for _, value in ipairs(var.values) do + if is_externally_accessible(value) then + no_values_externally_accessible = false + end + end + + if not var.accessed and no_values_externally_accessible then + warn_unaccessed_var(chstate, var, true) + end + + for _, value in ipairs(var.values) do + if not value.empty then + if not value.used then + if not value.mutated then + local overwriting_node + + if value.overwriting_item then + if value.overwriting_item ~= value.item then + overwriting_node = get_overwriting_lhs_node(value.overwriting_item, value) + end + else + overwriting_node = get_second_overwriting_lhs_node(value.item, value) + end + + warn_unused_value(chstate, value, overwriting_node) + elseif not is_externally_accessible(value) then + if var.accessed or not no_values_externally_accessible then + warn_unused_value(chstate, value) + end + end + end + end + end + end +end + +local function detect_unused_locals_in_line(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 the implicit top level vararg. + if var.node.line then + detect_unused_local(chstate, var) + end + end + end + end +end + +local function detect_unused_locals(chstate) + for _, line in ipairs(chstate.lines) do + detect_unused_locals_in_line(chstate, line) + end +end + +local function mark_reachable_lines(edges, marked, line) + for connected_line in pairs(edges[line]) do + if not marked[connected_line] then + marked[connected_line] = true + mark_reachable_lines(edges, marked, connected_line) + end + end +end + +local function detect_unused_rec_funcs(chstate) + -- Build a graph of usage relations of all closures. + -- Closure A is used by closure B iff either B is parent + -- of A and A is not assigned to a local/upvalue, or + -- B uses local/upvalue value that is A. + -- Closures not reachable from root closure are unused, + -- report corresponding values/variables if not done already. + + local line = chstate.top_line + + -- Initialize edges maps. + local forward_edges = {[line] = {}} + local backward_edges = {[line] = {}} + + for _, nested_line in ipairs(line.lines) do + forward_edges[nested_line] = {} + backward_edges[nested_line] = {} + end + + -- Add edges leading to each nested line. + for _, nested_line in ipairs(line.lines) do + if nested_line.node.value then + for using_line in pairs(nested_line.node.value.using_lines) do + forward_edges[using_line][nested_line] = true + backward_edges[nested_line][using_line] = true + end + elseif nested_line.parent then + forward_edges[nested_line.parent][nested_line] = true + backward_edges[nested_line][nested_line.parent] = true + end + end + + -- Recursively mark all closures reachable from root closure and unused closures. + -- Closures reachable from main chunk are used; closure reachable from unused closures + -- depend on that closure; that is, fixing warning about parent unused closure + -- fixes warning about the child one, so issuing a warning for the child is superfluous. + local marked = {[line] = true} + mark_reachable_lines(forward_edges, marked, line) + + for _, nested_line in ipairs(line.lines) do + if nested_line.node.value and not nested_line.node.value.used then + marked[nested_line] = true + mark_reachable_lines(forward_edges, marked, nested_line) + end + end + + -- Deal with unused closures. + for _, nested_line in ipairs(line.lines) do + local value = nested_line.node.value + + if value and value.used and not marked[nested_line] then + -- This closure is used by some closure, but is not marked as reachable + -- from main chunk or any of reported closures. + -- Find candidate group of mutually recursive functions containing this one: + -- mark sets of closures reachable from it by forward and backward edges, + -- intersect them. Ignore already marked closures in the process to avoid + -- issuing superfluous, dependent warnings. + local forward_marked = setmetatable({}, {__index = marked}) + local backward_marked = setmetatable({}, {__index = marked}) + mark_reachable_lines(forward_edges, forward_marked, nested_line) + mark_reachable_lines(backward_edges, backward_marked, nested_line) + + -- Iterate over closures in the group. + for mut_rec_line in pairs(forward_marked) do + if rawget(backward_marked, mut_rec_line) then + marked[mut_rec_line] = true + value = mut_rec_line.node.value + + if value then + -- Report this closure as self recursive or mutually recursive. + local is_self_recursive = forward_edges[mut_rec_line][mut_rec_line] + + if is_function_var(value.var) then + chstate:warn_value("211", value, { + func = true, + mutually_recursive = not is_self_recursive or nil, + recursive = is_self_recursive or nil + }) + else + chstate:warn_value("311", value) + end + end + end + end + end + end +end + +-- Warns about unused local variables and their values as well as locals that +-- are accessed but never set or set but never accessed. +-- Warns about unused recursive functions. +function stage.run(chstate) + detect_unused_locals(chstate) + detect_unused_rec_funcs(chstate) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/linearize.lua luacheck-0.23.0/src/luacheck/stages/linearize.lua --- luacheck-0.22.0/src/luacheck/stages/linearize.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/linearize.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,708 @@ +local parser = require "luacheck.parser" +local utils = require "luacheck.utils" + +local stage = {} + +local function redefined_warning(message_format) + return { + message_format = message_format, + fields = {"name", "prev_line", "prev_column", "prev_end_column", "self"} + } +end + +stage.warnings = { + ["411"] = redefined_warning("variable {name!} was previously defined on line {prev_line}"), + ["412"] = redefined_warning("variable {name!} was previously defined as an argument on line {prev_line}"), + ["413"] = redefined_warning("variable {name!} was previously defined as a loop variable on line {prev_line}"), + ["421"] = redefined_warning("shadowing definition of variable {name!} on line {prev_line}"), + ["422"] = redefined_warning("shadowing definition of argument {name!} on line {prev_line}"), + ["423"] = redefined_warning("shadowing definition of loop variable {name!} on line {prev_line}"), + ["431"] = redefined_warning("shadowing upvalue {name!} on line {prev_line}"), + ["432"] = redefined_warning("shadowing upvalue argument {name!} on line {prev_line}"), + ["433"] = redefined_warning("shadowing upvalue loop variable {name!} on line {prev_line}"), + ["521"] = {message_format = "unused label {label!}", fields = {"label"}} +} + +local type_codes = { + var = "1", + func = "1", + arg = "2", + loop = "3", + loopi = "3" +} + +local function warn_redefined(chstate, var, prev_var, is_same_scope) + local code = "4" .. (is_same_scope and "1" or var.line == prev_var.line and "2" or "3") .. type_codes[prev_var.type] + + chstate:warn_var(code, var, { + self = var.self and prev_var.self, + prev_line = prev_var.node.line, + prev_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.offset), + prev_end_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.end_offset) + }) +end + +local function warn_unused_label(chstate, label) + chstate:warn_range("521", label.range, { + label = label.name + }) +end + +local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"}) + +local Line = utils.class() + +function Line:__init(node, parent, value) + -- Maps variables to arrays of accessing items. + self.accessed_upvalues = {} + -- Maps variables to arrays of mutating items. + self.mutated_upvalues = {} + -- Maps variables to arays of setting items. + self.set_upvalues = {} + self.lines = {} + self.node = node + self.parent = parent + self.value = value + self.items = utils.Stack() +end + +-- Calls callback with line, index, item, ... for each item reachable from starting item. +-- `visited` is a set of already visited indexes. +-- Callback can return true to stop walking from current item. +function Line:walk(visited, index, callback, ...) + if visited[index] then + return + end + + visited[index] = true + + local item = self.items[index] + + if callback(self, index, item, ...) then + return + end + + if not item then + return + elseif item.tag == "Jump" then + return self:walk(visited, item.to, callback, ...) + elseif item.tag == "Cjump" then + self:walk(visited, item.to, callback, ...) + end + + return self:walk(visited, index + 1, callback, ...) +end + +local function new_scope(line) + return { + vars = {}, + labels = {}, + gotos = {}, + line = line + } +end + +local function new_var(line, node, type_) + return { + name = node[1], + node = node, + type = type_, + self = node.implicit, + line = line, + scope_start = line.items.size + 1, + values = {} + } +end + +local function new_value(var_node, value_node, item, is_init) + local value = { + var = var_node.var, + var_node = var_node, + type = is_init and var_node.var.type or "var", + node = value_node, + using_lines = {}, + empty = is_init and not value_node and (var_node.var.type == "var"), + item = item + } + + if value_node and value_node.tag == "Function" then + value.type = "func" + value_node.value = value + end + + return value +end + +local function new_label(line, name, range) + return { + name = name, + range = range, + index = line.items.size + 1 + } +end + +local function new_goto(name, jump, range) + return { + name = name, + jump = jump, + range = range + } +end + +local function new_jump_item(is_conditional) + return { + tag = is_conditional and "Cjump" or "Jump" + } +end + +local function new_eval_item(node) + return { + tag = "Eval", + node = node, + accesses = {}, + used_values = {}, + lines = {} + } +end + +local function new_noop_item(node, loop_end) + return { + tag = "Noop", + node = node, + loop_end = loop_end + } +end + +local function new_local_item(node) + return { + tag = "Local", + node = node, + lhs = node[1], + rhs = node[2], + accesses = node[2] and {}, + used_values = node[2] and {}, + lines = node[2] and {} + } +end + +local function new_set_item(node) + return { + tag = "Set", + node = node, + lhs = node[1], + rhs = node[2], + accesses = {}, + mutations = {}, + 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 + parser.syntax_error("'break' is not inside a loop", goto_.range) + else + parser.syntax_error(("no visible label '%s'"):format(goto_.name), goto_.range) + 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 + warn_unused_label(self.chstate, 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 is_same_scope = self.scopes.top.vars[var.name] + + if var.name ~= "..." then + warn_redefined(self.chstate, var, prev_var, is_same_scope) + end + + if is_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) + if not node.var then + node.var = self:resolve_var(node[1]) + end + + return node.var +end + +function LinState:register_label(name, range) + local prev_label = self.scopes.top.labels[name] + + if prev_label then + assert(not pseudo_labels[name]) + parser.syntax_error(("label '%s' already defined on line %d"):format( + name, prev_label.range.line), range, prev_label.range) + end + + self.scopes.top.labels[name] = new_label(self.lines.top, name, range) +end + +function LinState:emit(item) + self.lines.top.items:push(item) +end + +function LinState:emit_goto(name, is_conditional, range) + local jump = new_jump_item(is_conditional) + self:emit(jump) + table.insert(self.scopes.top.gotos, new_goto(name, jump, range)) +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: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:emit_block(node[i + 1]) + self:emit_goto("end") + self:register_label("else") + self:leave_scope() + end + + if #node % 2 == 1 then + 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) +end + +function LinState:emit_stmt_Goto(node) + self:emit_noop(node) + self:emit_goto(node[1], false, node) +end + +function LinState:emit_stmt_Break(node) + self:emit_goto("break", false, node) +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) + local item = new_local_item(node) + 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) + self:register_var(node[1][1], "var") + self:emit(item) + self:scan_expr(item, node[2][1]) +end + +function LinState:emit_stmt_Set(node) + local item = new_set_item(node) + self:scan_exprs(item, node[2]) + + for _, expr in ipairs(node[1]) do + if expr.tag == "Id" then + local var = self:check_var(expr) + + if var then + self:register_upvalue_action(item, var, "set_upvalues") + end + else + assert(expr.tag == "Index") + self:scan_lhs_index(item, expr) + 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, key) + 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, "accessed_upvalues") +end + +function LinState:mark_mutation(item, node) + node.var.mutated = true + + if not item.mutations[node.var] then + item.mutations[node.var] = {} + end + + table.insert(item.mutations[node.var], node) + self:register_upvalue_action(item, node.var, "mutated_upvalues") +end + +function LinState:scan_expr_Id(item, node) + if self:check_var(node) then + self:mark_access(item, node) + end +end + +function LinState:scan_expr_Dots(item, node) + local dots = self:check_var(node) + + if not dots or dots.line ~= self.lines.top then + parser.syntax_error("cannot use '...' outside a vararg function", node) + end + + self:mark_access(item, node) +end + +function LinState:scan_lhs_index(item, node) + if node[1].tag == "Id" then + if self:check_var(node[1]) then + self:mark_mutation(item, node[1]) + end + elseif node[1].tag == "Index" then + self:scan_lhs_index(item, node[1]) + else + self:scan_expr(item, node[1]) + end + + self:scan_expr(item, node[2]) +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_Table = LinState.scan_exprs +LinState.scan_expr_Pair = 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, 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(node) + self.lines:push(Line(node, self.lines.top)) + self:enter_scope() + self:emit(new_local_item({node[1]})) + self:enter_scope() + self:register_vars(node[1], "arg") + self:emit_stmts(node[2]) + 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) + table.insert(item.lines, line) + + for _, nested_line in ipairs(line.lines) do + table.insert(item.lines, nested_line) + end +end + +-- Builds linear representation (line) of AST and assigns it as `chstate.top_line`. +-- Assings an array of all lines as `chstate.lines`. +-- Adds warnings for redefined/shadowed locals and unused labels. +function stage.run(chstate) + local linstate = LinState(chstate) + chstate.top_line = linstate:build_line({{{tag = "Dots", "..."}}, chstate.ast}) + assert(linstate.lines.size == 0) + assert(linstate.scopes.size == 0) + + chstate.lines = {chstate.top_line} + + for _, nested_line in ipairs(chstate.top_line.lines) do + table.insert(chstate.lines, nested_line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/name_functions.lua luacheck-0.23.0/src/luacheck/stages/name_functions.lua --- luacheck-0.22.0/src/luacheck/stages/name_functions.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/name_functions.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,71 @@ +local stage = {} + +local function get_index_name(base_name, key_node) + if key_node.tag == "String" then + return base_name .. "." .. key_node[1] + end +end + +local function get_full_field_name(node) + if node.tag == "Id" then + return node[1] + elseif node.tag == "Index" then + local base_name = get_full_field_name(node[1]) + return base_name and get_index_name(base_name, node[2]) + end +end + +local handle_node + +local function handle_nodes(nodes) + for _, node in ipairs(nodes) do + if type(node) == "table" then + handle_node(node) + end + end +end + +function handle_node(node, name) + if node.tag == "Function" then + node.name = name + handle_nodes(node[2]) + elseif node.tag == "Set" or node.tag == "Local" or node.tag == "Localrec" then + local lhs = node[1] + local rhs = node[2] + + -- No need to handle LHS if there is no RHS, it's always just a list of locals in that case. + if rhs then + handle_nodes(lhs) + + for index, rhs_node in ipairs(rhs) do + local lhs_node = lhs[index] + local field_name = lhs_node and get_full_field_name(lhs_node) + handle_node(rhs_node, field_name) + end + end + elseif node.tag == "Table" and name then + for _, pair_node in ipairs(node) do + if pair_node.tag == "Pair" then + local key_node = pair_node[1] + local value_node = pair_node[2] + handle_node(key_node) + handle_node(value_node, get_index_name(name, key_node)) + else + handle_node(pair_node) + end + end + else + handle_nodes(node) + end +end + +-- Adds `name` field to `Function` ast nodes when possible: +-- * Function assigned to a variable (doesn't matter if local or global): "foo". +-- * Function assigned to a field: "foo.bar.baz". +-- Function can be in a table assigned to a variable or a field, e.g. `foo.bar = {baz = function() ... end}`. +-- * Otherwise: `nil`. +function stage.run(chstate) + handle_nodes(chstate.ast) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/parse_inline_options.lua luacheck-0.23.0/src/luacheck/stages/parse_inline_options.lua --- luacheck-0.22.0/src/luacheck/stages/parse_inline_options.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/parse_inline_options.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,351 @@ +local options = require "luacheck.options" +local utils = require "luacheck.utils" + +local stage = {} + +stage.warnings = { + -- Also produced during filtering for options that did not pass validation. + ["021"] = {message_format = "{msg}", fields = {"msg"}}, + ["022"] = {message_format = "unpaired push directive", fields = {}}, + ["023"] = {message_format = "unpaired pop directive", fields = {}} +} + +stage.inline_option_fields = {"line", "pop_count", "options", "column", "end_column"} + +local limit_opts = utils.array_to_set({"max_line_length", "max_code_line_length", "max_string_line_length", + "max_comment_line_length", "max_cyclomatic_complexity"}) + +local function is_valid_option_name(name) + if name == "std" or options.variadic_inline_options[name] then + return true + end + + name = name:gsub("^no_", "") + return options.nullary_inline_options[name] or limit_opts[name] +end + +-- Splits a token array for an inline option invocation into +-- option name and argument array, or nil if invocation is invalid. +local function split_invocation(tokens) + -- Name of the option can be split into several space separated tokens. + -- Since some valid names are prefixes of some other names + -- (e.g. `unused` and `unused arguments`), the longest prefix of token + -- array that is a valid option name should be considered. + local cur_name + local last_valid_name + local last_valid_name_end_index + + for i, token in ipairs(tokens) do + cur_name = cur_name and (cur_name .. "_" .. token) or token + + if is_valid_option_name(cur_name) then + last_valid_name = cur_name + last_valid_name_end_index = i + end + end + + if not last_valid_name then + return + end + + local args = {} + + for i = last_valid_name_end_index + 1, #tokens do + table.insert(args, tokens[i]) + end + + return last_valid_name, args +end + +local function unexpected_num_args(name, args, expected) + return ("inline option '%s' expects %d argument%s, %d given"):format( + name, expected, expected == 1 and "" or "s", #args) +end + +-- Parses inline option body, returns options or nil and error message. +local function parse_options(body) + local opts = {} + + local parts = utils.split(body, ",") + + for _, name_and_args in ipairs(parts) do + local tokens = utils.split(name_and_args) + local name, args = split_invocation(tokens) + + if not name then + if #tokens == 0 then + return nil, (#parts == 1) and "empty inline option" or "empty inline option invocation" + else + return nil, ("unknown inline option '%s'"):format(table.concat(tokens, " ")) + end + end + + if name == "std" then + if #args ~= 1 then + return nil, unexpected_num_args(name, args, 1) + end + + opts.std = args[1] + elseif name == "ignore" and #args == 0 then + opts.ignore = {".*"} + elseif options.variadic_inline_options[name] then + opts[name] = args + else + local full_name = name:gsub("_", " ") + local subs + name, subs = name:gsub("^no_", "") + local flag = subs == 0 + + if options.nullary_inline_options[name] then + if #args ~= 0 then + return nil, unexpected_num_args(full_name, args, 0) + end + + opts[name] = flag + else + assert(limit_opts[name]) + + if flag then + if #args ~= 1 then + return nil, unexpected_num_args(full_name, args, 1) + end + + local value = tonumber(args[1]) + + if not value then + return nil, ("inline option '%s' expects number as argument"):format(name) + end + + opts[name] = value + else + if #args ~= 0 then + return nil, unexpected_num_args(full_name, args, 0) + end + + opts[name] = false + end + end + end + end + + return opts +end + +-- Parses comment contents, returns up to two `options` values (tables or "push" or "pop"). +-- On an invalid inline comment returns nil and an error message. +local function parse_inline_comment(comment_contents) + local body = utils.after(utils.strip(comment_contents), "^luacheck:") + + if not body then + return + end + + local opts1, opts2 + + -- Remove comments in balanced parens. + body = utils.strip((body:gsub("%b()", " "))) + local after_push = body:match("^push%s+(.*)") + + if after_push then + opts2 = "push" + body = after_push + elseif body == "push" or body == "pop" then + return body + end + + local err_msg + opts1, err_msg = parse_options(body) + return opts1, err_msg or opts2 +end + +-- Returns an array of tables with column range info and an `options` field +-- containing a table of options or "push" or "pop". +-- Warns about invalid inline option comments. +local function parse_inline_comments(chstate) + local res = {} + + for _, comment in ipairs(chstate.comments) do + local opts1, opts2 = parse_inline_comment(comment.contents) + + if opts1 then + table.insert(res, { + line = comment.line, + column = chstate:offset_to_column(comment.line, comment.offset), + end_column = chstate:offset_to_column(comment.line, comment.end_offset), + options = opts1 + }) + + if opts2 then + table.insert(res, { + line = comment.line, + column = chstate:offset_to_column(comment.line, comment.offset), + end_column = chstate:offset_to_column(comment.line, comment.end_offset), + options = opts2 + }) + end + elseif opts2 then + chstate:warn_range("021", comment, {msg = opts2}) + end + end + + return res +end + +-- Adds a table with `line`, `column`, and `options` fields to given array. +-- For each function a table with `options` set to "push" for the function start +-- and a talbe with `options` set to "pop" for the function end are added. +local function add_function_boundaries(inline_options_and_boundaries, chstate) + for _, line in ipairs(chstate.top_line.lines) do + local fn_node = line.node + + table.insert(inline_options_and_boundaries, { + line = fn_node.line, + column = chstate:offset_to_column(fn_node.line, fn_node.offset), + options = "push" + }) + + table.insert(inline_options_and_boundaries, { + line = fn_node.end_range.line, + column = chstate:offset_to_column(fn_node.end_range.line, fn_node.end_range.offset), + options = "pop" + }) + end +end + +local function get_order(t) + if t.options == "push" then + return 1 + elseif t.options == "pop" then + return 3 + else + return 2 + end +end + +local function options_and_boundaries_comparator(t1, t2) + if t1.line ~= t2.line then + return t1.line < t2.line + end + + -- For options and boundaries on the same line, all pushes are applied before options before pops. + -- (Valid pops will be moved to the start of the next line later.) + local order1 = get_order(t1) + local order2 = get_order(t2) + + if order1 ~= order2 then + return order1 < order2 + else + return t1.column < t2.column + end +end + +-- Applies bounadaries withing `inline_options_and_boundaries` to replace them with pop count +-- instructions in the resulting array. +-- Comments on lines with code are popped at the end of line. +-- Warns about unpaired push and pop directives. +local function apply_boundaries(chstate, inline_options_and_boundaries) + local res = {} + local res_last + + -- While iterating over inline options and boundaries track push + -- boundaries that were not popped yet plus the number of options + -- that would be on the option stack after applying all already + -- processed option table pushes and pops. + local pushes = utils.Stack() + local push_option_counts = utils.Stack() + local option_count = 0 + + for _, item in ipairs(inline_options_and_boundaries) do + if item.options == "push" then + pushes:push(item) + push_option_counts:push(option_count) + elseif item.options == "pop" then + -- Function boundaries are implicit, don't allow inline options to pop + -- them, don't allow function boundaries to pop inline option pushes either. + -- Inline options boundaries have end_column, function boundaries don't. + if not pushes.top or (item.end_column and not pushes.top.end_column) then + -- Inline option pop against nothing or a function push, mark as unpaired. + chstate:warn_column_range("023", item) + else + if not item.end_column then + -- Function pop, remove any unpaired inline option pushes. + while pushes.top and pushes.top.end_column do + chstate:warn_column_range("022", pushes.top) + pushes:pop() + push_option_counts:pop() + end + end + + pushes:pop() + local prev_option_count = push_option_counts:pop() + local pop_count = option_count - prev_option_count + + if pop_count > 0 then + -- Place the pop instruction at the start of the next line so that getting option stack + -- for a line amounts to applying both the pop instruction and the option push for the line. + local line = item.line + 1 + + -- Collapse with a previous table if it's on the same line. It can only be a pop count table. + if res_last and res_last.line == line then + res_last.pop_count = res_last.pop_count + pop_count + else + res_last = { + line = line, + pop_count = pop_count + } + + table.insert(res, res_last) + end + end + + -- Update option stack size for this pop. + option_count = prev_option_count + end + else + -- Inline options table. Check if there is a pop count table for this line already. + if res_last and res_last.line == item.line then + res_last.options = item.options + res_last.column = item.column + res_last.end_column = item.end_column + else + res_last = item + table.insert(res, item) + end + + if chstate.code_lines[item.line] then + -- Inline comment on a line with some code, immediately pop it. + res_last = { + line = item.line + 1, + pop_count = 1 + } + table.insert(res, res_last) + else + option_count = option_count + 1 + end + end + end + + -- Any remaining pushes are unpaired inline comments from the main chunk. + while pushes.top do + chstate:warn_column_range("022", pushes:pop()) + end + + return res +end + +-- Warns about invalid inline options. +-- Sets `chstate.inline_options` to an array of tables that describe the way inline option tables +-- are pushed onto and popped from the option stack when iterating over lines. +-- Each table has field `line` that the array is sorted by and also ether or both sets of fields: +-- * `pop_count` - refers to a number of option tables that should be popped from the stack before processing +-- warnings on this line. +-- * `options`, `column`, `end_column` - refers to an option table that should be pushed onto the stack +-- before processing warnings on this line but after popping tables if `pop_count` is present. +function stage.run(chstate) + local inline_options_and_boundaries = parse_inline_comments(chstate) + add_function_boundaries(inline_options_and_boundaries, chstate) + table.sort(inline_options_and_boundaries, options_and_boundaries_comparator) + chstate.inline_options = apply_boundaries(chstate, inline_options_and_boundaries) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/parse.lua luacheck-0.23.0/src/luacheck/stages/parse.lua --- luacheck-0.22.0/src/luacheck/stages/parse.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/parse.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,19 @@ +local decoder = require "luacheck.decoder" +local parser = require "luacheck.parser" + +local stage = {} + +function stage.run(chstate) + chstate.source = decoder.decode(chstate.source_bytes) + chstate.line_offsets = {} + chstate.line_lengths = {} + local ast, comments, code_lines, line_endings, useless_semicolons = parser.parse( + chstate.source, chstate.line_offsets, chstate.line_lengths) + chstate.ast = ast + chstate.comments = comments + chstate.code_lines = code_lines + chstate.line_endings = line_endings + chstate.useless_semicolons = useless_semicolons +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/resolve_locals.lua luacheck-0.23.0/src/luacheck/stages/resolve_locals.lua --- luacheck-0.22.0/src/luacheck/stages/resolve_locals.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/resolve_locals.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,216 @@ +local stage = {} + +-- The main part of analysis is connecting assignments to locals or upvalues +-- with accesses that may use the assigned value. +-- Accesses and assignments are split into two groups based on whether they happen +-- in the closure that defines subject local variable (main assignment, main access) +-- or in some nested closure (closure assignment, closure access). +-- To avoid false positives, it's assumed that a closure may be called at any point +-- starting from expression that creates it. +-- Additionally, all operations on upvalues are considered in bulk, as in, +-- when a closure is called, it's assumed that any subset of its upvalue assignments +-- and accesses may happen, in any order. + +-- Assignments and accesses are connected based on whether they can reach each other. +-- A main assignment is connected with a main access when the assignment can reach the access. +-- A main assignment is connected with a closure access when the assignment can reach the closure creation +-- or the closure creation can reach the assignment. +-- A closure assignment is connected with a main access when the closure creation can reach the access. +-- A closure assignment is connected with a closure access when either closure creation can reach the other one. + +-- To determine what flow graph nodes an assignment or a closure creation can reach, +-- they are independently propagated along the graph. +-- Closure creation propagation is not bounded. +-- Main assignment propagation is bounded by entrance and exit conditions for each reached flow graph node. +-- Entrance condition checks that target local variable is still in scope. If entrance condition fails, +-- nothing in the node can refer to the variable, and the scope can't be reentered later. +-- So, in this case, assignment does not reach the node, and propagation does not continue. +-- Exit condition checks that target local variable is not overwritten by an assignment in the node. +-- If it fails, the assignment still reaches the node (because all accesses in a node are evaluated before any +-- assignments take effect), but propagation does not continue. + +local function register_value(values_per_var, var, value) + if not values_per_var[var] then + values_per_var[var] = {} + end + + table.insert(values_per_var[var], value) +end + +-- Called when assignment of `value` is connected to an access. +-- `item` contains the access, and `line` contains the item. +local function add_resolution(line, item, var, value, is_mutation) + register_value(item.used_values, var, value) + value[is_mutation and "mutated" or "used"] = true + value.using_lines[line] = true + + if value.secondaries then + value.secondaries.used = true + end +end + +-- Connects accesses in given items array with an assignment of `value`. +-- `items` may be `nil` instead of empty. +local function add_resolutions(line, items, var, value, is_mutation) + if not items then + return + end + + for _, item in ipairs(items) do + add_resolution(line, item, var, value, is_mutation) + end +end + +-- Connects all accesses (and mutations) in `access_line` with corresponding +-- assignments in `set_line`. +local function cross_resolve_closures(access_line, set_line) + for var, setting_items in pairs(set_line.set_upvalues) do + for _, setting_item in ipairs(setting_items) do + add_resolutions(access_line, access_line.accessed_upvalues[var], + var, setting_item.set_variables[var]) + add_resolutions(access_line, access_line.mutated_upvalues[var], + var, setting_item.set_variables[var], true) + end + end +end + +local function in_scope(var, index) + return (var.scope_start <= index) and (index <= var.scope_end) +end + +-- Called when main assignment propagation reaches a line item. +local function main_assignment_propagation_callback(line, index, item, var, value) + -- Check entrance condition. + if not in_scope(var, index) then + -- Assignment reaches the end of variable scope, so it can't be dominated by any assignment. + value.overwriting_item = false + return true + end + + -- Assignment reaches this item, apply its effect. + + -- Accesses (and mutations) of the variable can resolve to reaching assignment. + if item.accesses and item.accesses[var] then + add_resolution(line, item, var, value) + end + + if item.mutations and item.mutations[var] then + add_resolution(line, item, var, value, true) + end + + -- Accesses (and mutations) of the variable inside closures created in this item + -- can resolve to reaching assignment. + if item.lines then + for _, created_line in ipairs(item.lines) do + add_resolutions(created_line, created_line.accessed_upvalues[var], var, value) + add_resolutions(created_line, created_line.mutated_upvalues[var], var, value, true) + end + end + + -- Check exit condition. + if item.set_variables and item.set_variables[var] then + if value.overwriting_item ~= false then + if value.overwriting_item and value.overwriting_item ~= item then + value.overwriting_item = false + else + value.overwriting_item = item + end + end + + return true + end +end + +-- Connects main assignments with main accesses and closure accesses in reachable closures. +-- Additionally, sets `overwriting_item` field of values to an item with an assignment overwriting +-- the value, but only if the overwriting is not avoidable (i.e. it's impossible to reach end of function +-- from the first assignment without going through the second one). Otherwise value of the field may be +-- `false` or `nil`. +local function propagate_main_assignments(line) + for i, item in ipairs(line.items) do + if item.set_variables then + for var, value in pairs(item.set_variables) do + if var.line == line then + -- Assignments are not live at their own item, because assignments take effect only after all accesses + -- are evaluated. Items with assignments can't be jumps, so they have a single following item + -- with incremented index. + line:walk({}, i + 1, main_assignment_propagation_callback, var, value) + end + end + end + end +end + +-- Called when closure creation propagation reaches a line item. +local function closure_creation_propagation_callback(line, _, item, propagated_line) + if not item then + return true + end + + -- Closure creation reaches this item, apply its effects. + + -- Accesses (and mutations) of upvalues in the propagated closure + -- can resolve to assignments in the item. + if item.set_variables then + for var, value in pairs(item.set_variables) do + add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], var, value) + add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], var, value, true) + end + end + + if item.lines then + for _, created_line in ipairs(item.lines) do + -- Accesses (and mutations) of upvalues in the propagated closure + -- can resolve to assignments in closures created in the item. + cross_resolve_closures(propagated_line, created_line) + + -- Accesses (and mutations) of upvalues in closures created in the item + -- can resolve to assignments in the propagated closure. + cross_resolve_closures(created_line, propagated_line) + end + end + + -- Accesses (and mutations) of locals in the item can resolve + -- to assignments in the propagated closure. + for var, setting_items in pairs(propagated_line.set_upvalues) do + if item.accesses and item.accesses[var] then + for _, setting_item in ipairs(setting_items) do + add_resolution(line, item, var, setting_item.set_variables[var]) + end + end + + if item.mutations and item.mutations[var] then + for _, setting_item in ipairs(setting_items) do + add_resolution(line, item, var, setting_item.set_variables[var], true) + end + end + end +end + +-- Connects main assignments with closure accesses in reaching closures. +-- Connects closure assignments with main accesses and with closure accesses in reachable closures. +-- Connects closure accesses with closure assignments in reachable closures. +local function propagate_closure_creations(line) + for i, item in ipairs(line.items) do + if item.lines then + for _, created_line in ipairs(item.lines) do + -- Closures are live at the item they are created, as they can be called immediately. + line:walk({}, i, closure_creation_propagation_callback, created_line) + end + end + end +end + +local function analyze_line(line) + propagate_main_assignments(line) + propagate_closure_creations(line) +end + +-- Finds reaching assignments for all local variable accesses. +function stage.run(chstate) + for _, line in ipairs(chstate.lines) do + analyze_line(line) + end +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages/unwrap_parens.lua luacheck-0.23.0/src/luacheck/stages/unwrap_parens.lua --- luacheck-0.22.0/src/luacheck/stages/unwrap_parens.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages/unwrap_parens.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,52 @@ +local stage = {} + +-- Mutates an array of nodes and non-tables, unwrapping Paren nodes. +-- If list_start is given, tail Paren is not unwrapped if it's unpacking and past list_start index. +local function handle_nodes(nodes, list_start) + local num_nodes = #nodes + + for index = 1, num_nodes do + local node = nodes[index] + + if type(node) == "table" then + local tag = node.tag + + if tag == "Table" or tag == "Return" then + handle_nodes(node, 1) + elseif tag == "Call" then + handle_nodes(node, 2) + elseif tag == "Invoke" then + handle_nodes(node, 3) + elseif tag == "Forin" then + handle_nodes(node[2], 1) + handle_nodes(node[3]) + elseif tag == "Local" then + if node[2] then + handle_nodes(node[2]) + end + elseif tag == "Set" then + handle_nodes(node[1]) + handle_nodes(node[2], 1) + else + handle_nodes(node) + + if tag == "Paren" and (not list_start or index < list_start or index ~= num_nodes) then + local inner_node = node[1] + + if inner_node.tag ~= "Call" and inner_node.tag ~= "Invoke" and inner_node.tag ~= "Dots" then + nodes[index] = inner_node + end + end + end + end + end +end + +-- Mutates AST, unwrapping Paren nodes. +-- Paren nodes are preserved only when they matter: +-- at the ends of expression lists with potentially multi-value inner expressions. +function stage.run(chstate) + handle_nodes(chstate.ast) +end + +return stage diff -Nru luacheck-0.22.0/src/luacheck/stages.lua luacheck-0.23.0/src/luacheck/stages.lua --- luacheck-0.22.0/src/luacheck/stages.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/stages.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,75 @@ +local utils = require "luacheck.utils" + +local stages = {} + +-- Checking is organized into stages run one after another. +-- Each stage is in its own module and provides `run` function operating on a check state, +-- and optionally `warnings` table mapping issue codes to tables with fields `message_format` +-- containing format string for the issue or a function returning it given the issue, +-- and `fields` containing array of extra fields this warning can have. + +stages.names = { + "parse", + "unwrap_parens", + "linearize", + "parse_inline_options", + "name_functions", + "resolve_locals", + "detect_bad_whitespace", + "detect_cyclomatic_complexity", + "detect_empty_blocks", + "detect_empty_statements", + "detect_globals", + "detect_reversed_fornum_loops", + "detect_unbalanced_assignments", + "detect_uninit_accesses", + "detect_unreachable_code", + "detect_unused_fields", + "detect_unused_locals" +} + +stages.modules = {} + +for _, name in ipairs(stages.names) do + table.insert(stages.modules, require("luacheck.stages." .. name)) +end + +stages.warnings = {} + +local base_fields = {"code", "line", "column", "end_column"} + +local function register_warnings(warnings) + for code, warning in pairs(warnings) do + assert(not stages.warnings[code]) + assert(warning.message_format) + assert(warning.fields) + + local full_fields = utils.concat_arrays({base_fields, warning.fields}) + + stages.warnings[code] = { + message_format = warning.message_format, + fields = full_fields, + fields_set = utils.array_to_set(full_fields) + } + end +end + +-- Issues that do not originate from normal check stages (excluding global related ones). +register_warnings({ + ["011"] = {message_format = "{msg}", fields = {"msg", "prev_line", "prev_column", "prev_end_column"}}, + ["631"] = {message_format = "line is too long ({end_column} > {max_length})", fields = {}} +}) + +for _, stage_module in ipairs(stages.modules) do + if stage_module.warnings then + register_warnings(stage_module.warnings) + end +end + +function stages.run(chstate) + for _, stage_module in ipairs(stages.modules) do + stage_module.run(chstate) + end +end + +return stages diff -Nru luacheck-0.22.0/src/luacheck/unicode.lua luacheck-0.23.0/src/luacheck/unicode.lua --- luacheck-0.22.0/src/luacheck/unicode.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/unicode.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,36 @@ +local unicode_printability_boundaries = require "luacheck.unicode_printability_boundaries" + +local unicode = {} + +-- unicode_printability_boundaries is an array of first codepoints of +-- each continuous block of codepoints that are all printable or all not printable. + +function unicode.is_printable(codepoint) + -- Binary search for index of the first boundary less than or equal to given codepoint. + local floor_boundary_index + + -- Target index is always in [begin_index..end_index). + local begin_index = 1 + local end_index = #unicode_printability_boundaries + 1 + + while end_index - begin_index > 1 do + local mid_index = math.floor((begin_index + end_index) / 2) + local mid_codepoint = unicode_printability_boundaries[mid_index] + + if codepoint < mid_codepoint then + end_index = mid_index + elseif codepoint > mid_codepoint then + begin_index = mid_index + else + floor_boundary_index = mid_index + break + end + end + + floor_boundary_index = floor_boundary_index or begin_index + -- floor_boundary_index is the number of the block containing codepoint. + -- Printable and not printable blocks alternate and the first one is not printable (zero is not printable). + return floor_boundary_index % 2 == 0 +end + +return unicode diff -Nru luacheck-0.22.0/src/luacheck/unicode_printability_boundaries.lua luacheck-0.23.0/src/luacheck/unicode_printability_boundaries.lua --- luacheck-0.22.0/src/luacheck/unicode_printability_boundaries.lua 1970-01-01 00:00:00.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/unicode_printability_boundaries.lua 2018-09-18 19:43:27.000000000 +0000 @@ -0,0 +1,2 @@ +-- Autogenerated using data from https://www.unicode.org/Public/11.0.0/ucd/UnicodeData.txt +return {0,32,127,160,173,174,888,890,896,900,907,908,909,910,930,931,1328,1329,1367,1369,1419,1421,1424,1425,1480,1488,1515,1519,1525,1542,1564,1566,1757,1758,1806,1808,1867,1869,1970,1984,2043,2045,2094,2096,2111,2112,2140,2142,2143,2144,2155,2208,2229,2230,2238,2259,2274,2275,2436,2437,2445,2447,2449,2451,2473,2474,2481,2482,2483,2486,2490,2492,2501,2503,2505,2507,2511,2519,2520,2524,2526,2527,2532,2534,2559,2561,2564,2565,2571,2575,2577,2579,2601,2602,2609,2610,2612,2613,2615,2616,2618,2620,2621,2622,2627,2631,2633,2635,2638,2641,2642,2649,2653,2654,2655,2662,2679,2689,2692,2693,2702,2703,2706,2707,2729,2730,2737,2738,2740,2741,2746,2748,2758,2759,2762,2763,2766,2768,2769,2784,2788,2790,2802,2809,2816,2817,2820,2821,2829,2831,2833,2835,2857,2858,2865,2866,2868,2869,2874,2876,2885,2887,2889,2891,2894,2902,2904,2908,2910,2911,2916,2918,2936,2946,2948,2949,2955,2958,2961,2962,2966,2969,2971,2972,2973,2974,2976,2979,2981,2984,2987,2990,3002,3006,3011,3014,3017,3018,3022,3024,3025,3031,3032,3046,3067,3072,3085,3086,3089,3090,3113,3114,3130,3133,3141,3142,3145,3146,3150,3157,3159,3160,3163,3168,3172,3174,3184,3192,3213,3214,3217,3218,3241,3242,3252,3253,3258,3260,3269,3270,3273,3274,3278,3285,3287,3294,3295,3296,3300,3302,3312,3313,3315,3328,3332,3333,3341,3342,3345,3346,3397,3398,3401,3402,3408,3412,3428,3430,3456,3458,3460,3461,3479,3482,3506,3507,3516,3517,3518,3520,3527,3530,3531,3535,3541,3542,3543,3544,3552,3558,3568,3570,3573,3585,3643,3647,3676,3713,3715,3716,3717,3719,3721,3722,3723,3725,3726,3732,3736,3737,3744,3745,3748,3749,3750,3751,3752,3754,3756,3757,3770,3771,3774,3776,3781,3782,3783,3784,3790,3792,3802,3804,3808,3840,3912,3913,3949,3953,3992,3993,4029,4030,4045,4046,4059,4096,4294,4295,4296,4301,4302,4304,4681,4682,4686,4688,4695,4696,4697,4698,4702,4704,4745,4746,4750,4752,4785,4786,4790,4792,4799,4800,4801,4802,4806,4808,4823,4824,4881,4882,4886,4888,4955,4957,4989,4992,5018,5024,5110,5112,5118,5120,5789,5792,5881,5888,5901,5902,5909,5920,5943,5952,5972,5984,5997,5998,6001,6002,6004,6016,6110,6112,6122,6128,6138,6144,6158,6160,6170,6176,6265,6272,6315,6320,6390,6400,6431,6432,6444,6448,6460,6464,6465,6468,6510,6512,6517,6528,6572,6576,6602,6608,6619,6622,6684,6686,6751,6752,6781,6783,6794,6800,6810,6816,6830,6832,6847,6912,6988,6992,7037,7040,7156,7164,7224,7227,7242,7245,7305,7312,7355,7357,7368,7376,7418,7424,7674,7675,7958,7960,7966,7968,8006,8008,8014,8016,8024,8025,8026,8027,8028,8029,8030,8031,8062,8064,8117,8118,8133,8134,8148,8150,8156,8157,8176,8178,8181,8182,8191,8192,8203,8208,8232,8239,8288,8304,8306,8308,8335,8336,8349,8352,8384,8400,8433,8448,8588,8592,9255,9280,9291,9312,11124,11126,11158,11160,11209,11210,11263,11264,11311,11312,11359,11360,11508,11513,11558,11559,11560,11565,11566,11568,11624,11631,11633,11647,11671,11680,11687,11688,11695,11696,11703,11704,11711,11712,11719,11720,11727,11728,11735,11736,11743,11744,11855,11904,11930,11931,12020,12032,12246,12272,12284,12288,12352,12353,12439,12441,12544,12549,12592,12593,12687,12688,12731,12736,12772,12784,12831,12832,13055,13056,19894,19904,40944,40960,42125,42128,42183,42192,42540,42560,42744,42752,42938,42999,43052,43056,43066,43072,43128,43136,43206,43214,43226,43232,43348,43359,43389,43392,43470,43471,43482,43486,43519,43520,43575,43584,43598,43600,43610,43612,43715,43739,43767,43777,43783,43785,43791,43793,43799,43808,43815,43816,43823,43824,43878,43888,44014,44016,44026,44032,55204,55216,55239,55243,55292,63744,64110,64112,64218,64256,64263,64275,64280,64285,64311,64312,64317,64318,64319,64320,64322,64323,64325,64326,64450,64467,64832,64848,64912,64914,64968,65008,65022,65024,65050,65056,65107,65108,65127,65128,65132,65136,65141,65142,65277,65281,65471,65474,65480,65482,65488,65490,65496,65498,65501,65504,65511,65512,65519,65532,65534,65536,65548,65549,65575,65576,65595,65596,65598,65599,65614,65616,65630,65664,65787,65792,65795,65799,65844,65847,65935,65936,65948,65952,65953,66000,66046,66176,66205,66208,66257,66272,66300,66304,66340,66349,66379,66384,66427,66432,66462,66463,66500,66504,66518,66560,66718,66720,66730,66736,66772,66776,66812,66816,66856,66864,66916,66927,66928,67072,67383,67392,67414,67424,67432,67584,67590,67592,67593,67594,67638,67639,67641,67644,67645,67647,67670,67671,67743,67751,67760,67808,67827,67828,67830,67835,67868,67871,67898,67903,67904,67968,68024,68028,68048,68050,68100,68101,68103,68108,68116,68117,68120,68121,68150,68152,68155,68159,68169,68176,68185,68192,68256,68288,68327,68331,68343,68352,68406,68409,68438,68440,68467,68472,68498,68505,68509,68521,68528,68608,68681,68736,68787,68800,68851,68858,68904,68912,68922,69216,69247,69376,69416,69424,69466,69632,69710,69714,69744,69759,69821,69822,69826,69840,69865,69872,69882,69888,69941,69942,69959,69968,70007,70016,70094,70096,70112,70113,70133,70144,70162,70163,70207,70272,70279,70280,70281,70282,70286,70287,70302,70303,70314,70320,70379,70384,70394,70400,70404,70405,70413,70415,70417,70419,70441,70442,70449,70450,70452,70453,70458,70459,70469,70471,70473,70475,70478,70480,70481,70487,70488,70493,70500,70502,70509,70512,70517,70656,70746,70747,70748,70749,70751,70784,70856,70864,70874,71040,71094,71096,71134,71168,71237,71248,71258,71264,71277,71296,71352,71360,71370,71424,71451,71453,71468,71472,71488,71680,71740,71840,71923,71935,71936,72192,72264,72272,72324,72326,72355,72384,72441,72704,72713,72714,72759,72760,72774,72784,72813,72816,72848,72850,72872,72873,72887,72960,72967,72968,72970,72971,73015,73018,73019,73020,73022,73023,73032,73040,73050,73056,73062,73063,73065,73066,73103,73104,73106,73107,73113,73120,73130,73440,73465,73728,74650,74752,74863,74864,74869,74880,75076,77824,78895,82944,83527,92160,92729,92736,92767,92768,92778,92782,92784,92880,92910,92912,92918,92928,92998,93008,93018,93019,93026,93027,93048,93053,93072,93760,93851,93952,94021,94032,94079,94095,94112,94176,94178,94208,100338,100352,101107,110592,110879,110960,111356,113664,113771,113776,113789,113792,113801,113808,113818,113820,113824,118784,119030,119040,119079,119081,119155,119163,119273,119296,119366,119520,119540,119552,119639,119648,119673,119808,119893,119894,119965,119966,119968,119970,119971,119973,119975,119977,119981,119982,119994,119995,119996,119997,120004,120005,120070,120071,120075,120077,120085,120086,120093,120094,120122,120123,120127,120128,120133,120134,120135,120138,120145,120146,120486,120488,120780,120782,121484,121499,121504,121505,121520,122880,122887,122888,122905,122907,122914,122915,122917,122918,122923,124928,125125,125127,125143,125184,125259,125264,125274,125278,125280,126065,126133,126464,126468,126469,126496,126497,126499,126500,126501,126503,126504,126505,126515,126516,126520,126521,126522,126523,126524,126530,126531,126535,126536,126537,126538,126539,126540,126541,126544,126545,126547,126548,126549,126551,126552,126553,126554,126555,126556,126557,126558,126559,126560,126561,126563,126564,126565,126567,126571,126572,126579,126580,126584,126585,126589,126590,126591,126592,126602,126603,126620,126625,126628,126629,126634,126635,126652,126704,126706,126976,127020,127024,127124,127136,127151,127153,127168,127169,127184,127185,127222,127232,127245,127248,127340,127344,127405,127462,127491,127504,127548,127552,127561,127568,127570,127584,127590,127744,128725,128736,128749,128752,128762,128768,128884,128896,128985,129024,129036,129040,129096,129104,129114,129120,129160,129168,129198,129280,129292,129296,129343,129344,129393,129395,129399,129402,129403,129404,129443,129456,129466,129472,129475,129488,129536,129632,129646,131072,173783,173824,177973,177984,178206,178208,183970,183984,191457,194560,195102,917760,918000,} diff -Nru luacheck-0.22.0/src/luacheck/utils.lua luacheck-0.23.0/src/luacheck/utils.lua --- luacheck-0.22.0/src/luacheck/utils.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/utils.lua 2018-09-18 19:43:27.000000000 +0000 @@ -120,12 +120,6 @@ return t1 end -function utils.remove(t1, t2) - for k in pairs(t2) do - t1[k] = nil - end -end - local class_metatable = {} function class_metatable.__call(class, ...) @@ -278,39 +272,6 @@ return parts end --- Splits a string into an array of lines. --- "\n", "\r", "\r\n", and "\n\r" are considered --- line endings to be consistent with Lua lexer. -function utils.split_lines(str) - local lines = {} - local pos = 1 - - while true do - local line_end_pos, _, line_end = str:find("([\n\r])", pos) - - if not line_end_pos then - break - end - - local line = str:sub(pos, line_end_pos - 1) - table.insert(lines, line) - - pos = line_end_pos + 1 - local next_char = str:sub(pos, pos) - - if next_char:match("[\n\r]") and next_char ~= line_end then - pos = pos + 1 - end - end - - if pos <= #str then - local last_line = str:sub(pos) - table.insert(lines, last_line) - end - - return lines -end - utils.InvalidPatternError = utils.class() function utils.InvalidPatternError:__init(err, pattern) diff -Nru luacheck-0.22.0/src/luacheck/version.lua luacheck-0.23.0/src/luacheck/version.lua --- luacheck-0.22.0/src/luacheck/version.lua 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/src/luacheck/version.lua 2018-09-18 19:43:27.000000000 +0000 @@ -1,6 +1,8 @@ +local argparse = require "argparse" +local lfs = require "lfs" local luacheck = require "luacheck" -local fs = require "luacheck.fs" local multithreading = require "luacheck.multithreading" +local utils = require "luacheck.utils" local version = {} @@ -8,16 +10,15 @@ if rawget(_G, "jit") then version.lua = rawget(_G, "jit").version +elseif _VERSION:find("^Lua ") then + version.lua = "PUC-Rio " .. _VERSION else version.lua = _VERSION end -if fs.has_lfs then - local lfs = require "lfs" - version.lfs = lfs._VERSION -else - version.lfs = "Not found" -end +version.argparse = argparse.version + +version.lfs = utils.unprefix(lfs._VERSION, "LuaFileSystem ") if multithreading.has_lanes then version.lanes = multithreading.lanes.ABOUT.version @@ -28,7 +29,8 @@ version.string = ([[ Luacheck: %s Lua: %s +Argparse: %s LuaFileSystem: %s -LuaLanes: %s]]):format(version.luacheck, version.lua, version.lfs, version.lanes) +LuaLanes: %s]]):format(version.luacheck, version.lua, version.argparse, version.lfs, version.lanes) return version diff -Nru luacheck-0.22.0/.travis.yml luacheck-0.23.0/.travis.yml --- luacheck-0.22.0/.travis.yml 2018-05-09 12:02:06.000000000 +0000 +++ luacheck-0.23.0/.travis.yml 2018-09-18 19:43:27.000000000 +0000 @@ -1,40 +1,32 @@ -language: c +language: python sudo: false -cache: - directories: here +env: + - LUA="lua=5.1" + - LUA="lua=5.2" + - LUA="lua=5.3" + - LUA="luajit=2.0" + - LUA="luajit=2.1" -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: + - pip install hererocks + - pip install codecov + - hererocks here --$LUA -r latest + - source here/bin/activate + - luarocks install lanes + - luarocks install busted + - luarocks install cluacov + - luarocks install luautf8 + - luarocks install luasocket install: - - pip install --user hererocks - - pip install --user codecov - - hererocks here --$LUA -r https://github.com/mpeterv/luarocks@upgrade-install - - export PATH="$PWD/here/bin:$PATH" - - luarocks install lanes --upgrade --upgrade-deps - - luarocks install busted --upgrade --upgrade-deps - - luarocks install cluacov --upgrade --upgrade-deps + - luarocks make script: - busted -c - lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua luacheck-dev-1.rockspec -j2 - - lua -e 'package.preload.lfs=error;package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua src | grep 'I/O error' - lua -e 'package.preload.lanes=error;package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua --version | grep 'Not found' - - lua install.lua path/to/luacheck - - mv src src2 - - path/to/luacheck/bin/luacheck spec/*.lua - - mv src2 src + - lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua spec/*.lua after_script: - luacov