pax_global_header00006660000000000000000000000064131552466410014521gustar00rootroot0000000000000052 comment=8c8cfdb030a7b38165add80643660cbc9923c97b luacheck-0.21.1/000077500000000000000000000000001315524664100133615ustar00rootroot00000000000000luacheck-0.21.1/.gitignore000066400000000000000000000001211315524664100153430ustar00rootroot00000000000000bin/luacheck .luacheckcache luacov.stats.out luacov.report.out doc build package luacheck-0.21.1/.luacheckrc000066400000000000000000000002611315524664100154650ustar00rootroot00000000000000std = "min" cache = true include_files = {"src", "spec/*.lua", "install.lua"} files["spec/*_spec.lua"].std = "+busted" files["src/luacheck/argparse.lua"].max_line_length = 140 luacheck-0.21.1/.luacov000066400000000000000000000000571315524664100146550ustar00rootroot00000000000000return require "spec.helper".luacov_config("") luacheck-0.21.1/.travis.yml000066400000000000000000000024541315524664100154770ustar00rootroot00000000000000language: c sudo: false cache: directories: here 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" install: - pip install --user hererocks - 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 install luacov-coveralls --upgrade --upgrade-deps script: - busted -c - lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' -lluacov bin/luacheck.lua luacheck-scm-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 after_success: luacov-coveralls -v luacheck-0.21.1/CHANGELOG.md000066400000000000000000000410451315524664100151760ustar00rootroot00000000000000# Change Log ## 0.21.1 (2017-09-10) ### Fixes * Added missing definition of `ngx.ERROR` constant to `ngx_lua` std (#123). * Fixed unused values and initialized accesses not being reported when the access is in a closure defined in code path incompatible with the value assignment (#126). ## 0.21.0 (2017-09-04) ### New features and improvements * Column range for `line is too long` warning now starts at the first character beyond the length limit instead of the very first character of the line (#117). * Error messages for invalid inline options are now a bit better, including reason why an inline option invocation is invalid and what is the name of the option at fault. * `ngx_lua` std now contains full API definition for lua-nginx-module 0.10.10, so that operations on unknown fields within `ngx` global are now reported (#118). ### Fixes * `luacheck` no longer aborts on internal error while checking files in parallel. ## 0.20.0 (2017-05-08) ### Breaking changes * `luacheck` now exits with `2` instead of `1` if there are syntax errors or invalid inline options present. It now exits with `3` instead of `2` if I/O errors are present. It now exits with `4` instead of `3` on a critical error (#94). ### New features and improvements * If project-specific `.luacheckrc` is not found, `luacheck` will now use config from some global location if it is present there. Default global location is `%LOCALAPPDATA%\Luacheck\.luacheckrc` on Windows, `~/Library/Application Support/Luacheck/.luacheckrc` on OS X/macOS, and `$XDG_CONFIG_HOME/luacheck/.luacheckrc` or `~/.config/luacheck/.luacheckrc` on other systems. This behaviour can be tweaked with `--default-config` and `--no-default-config` options (#102). * New `--[no-]max-code-line-length`, `--[no-]max-string-line-length`, `--[no-]max-comment-line-length` CLI options and corresponding config and inline options that limit line length only for subsets of lines based on line type: string lines have their line endings within a string, comment lines have their line endings within a comment, other lines are code lines (#100). * New `love` std set containing globals added by Love2D framework (#108). * For warnings about unused values and fields, if the value is always overwritten by a single other value, location of the overwriting assignment is mentioned in the warning message (#106). * When attempting to check a directory while LuaFileSystem is not installed, `luacheck` now mentions that LuaFileSystem is required in the error message (#103). * Improved warning message for unbalanced assignment warnings (#104). ## 0.19.1 (2017-03-12) ### Miscellaneous * Added warning code for `line is too long` warnings to documentation (#98). * Added binary executable file for Windows. ## 0.19.0 (2017-03-03) ### Breaking changes * New format for defining standard sets of globals that can describe all allowed fields of each global. ### New features and improvements * Luacheck can now detect mutations and accesses of specific fields within globals. Standard global definitions have been updated to provide precise lists of allowed fields. This also works through local aliases (e.g. `local t = table; t.upsert()` produces a warning, but `local t = table; t.insert()` does not). * Default set of allowed globals is now equal to globals normally provided by version of Lua used to run Luacheck, instead of all globals set in the interpreter while it runs Luacheck. * All options that operate on lists of global names can now use field names as well. E.g. `--not-globals string.len` undefines standard field `string.len`. Additionally, config options `globals`, `new_globals`, `read_globals`, `new_read_globals` can use a table-based format to define trees of allowed fields. * Lines that are longer than some maximum length are now reported. Default limit is 120. Limit can be changed using `max_line_length` option. * Warnings related to trailing whitespace in comments and inside string literals now use separate warning codes. * Luacheck no longer reports a crash with a long traceback when interrupted, instead it simply exits with an error message. ### Fixes * Fixed inconsistent indentation not being detected on lines with trailing whitespace. ## 0.18.0 (2017-01-10) ### New features and improvements * Indirect mutations of read-only globals through local aliases are now detected (e.g. `local t = table; t.foo = "bar"`). * New CLI, config, and inline option `not_globals` for removing defined standard and custom globals (#88). * Custom globals defined as mutable using `globals` option can now be set to read-only using `read_globals` option in overwriting settings (previously `globals` had priority over `read_globals` even if `read_globals` was the last option used). * Luacheck exit codes are now documented. ### Fixes * Warnings that are explictly enabled by inline options are now correctly reported. E.g. `--luacheck: std none` now results in warnings for any used globals (#51). ## 0.17.1 (2016-12-22) ### Fixes * Fixed error when using cache and there are warnings with codes `314` or `521`. * Globals in `rockspec` std and `ngx` global in `ngx_lua` std are no longer read-only (#87). * Reverted changes to exit codes that conflicted with assumptions made by luacheck checker in Syntastic (#85). ## 0.17.0 (2016-11-18) ### New features and improvements * Trailing whitespace and inconsistent indentation (tabs after spaces) are now detected (#79). ## 0.16.3 (2016-10-27) ### Fixes * Fixed version number (#75). ## 0.16.2 (2016-10-25) ### Fixes * Fixed error in some cases when a function declaration is unreachable (#74). ## 0.16.1 (2016-09-29) ### Fixes * Fixed false positive for `variable/value is mutated but never accessed` warning when initial value of a variable comes from `X and Y`, `X or Y`, or `(X())` expression (#72). ## 0.16.0 (2016-08-30) ### New features and improvements * Local tables which are mutated but not used are now detected (#61). * Mutations of global variables with key chains of length > 1 are now correctly reported as mutations, not accesses. * Completely unused variables named `_` are now reported (#66). ### Fixes * `luacheck: ignore` now correctly filters out `5xx` and `314` warnings (#71). ## 0.15.1 (2016-06-09) ### Fixes * Fixed JUnit formatter not escaping XML entities (#62). ## 0.15.0 (2016-04-18) ### New features and improvements * New `rockspec` std set containing globals allowed in rockspecs (#55). ### Fixes * Fixed error when checking a file with a hexadecimal number using Lua 5.1 on Windows (#57). * Fixed luacheck using wrong path when checking a file in a subdirectory with single character name (#59). ## 0.14.0 (2016-02-25) ### New features and improvements * Duplicated keys in table literals are detected (#48). * Unused recursive and mutually recursive functions assigned to local variables are detected (#50). * Globs can be used to select paths when applying option overrides in config (#52). * Inline options can contain notes in parentheses. * `--jobs` option (multithreading) is used by default with LuaLanes found, number of threads used is set to number of available processing units. * Better error messages are provided on I/O and other errors when reading files, loading configs and rockspecs, etc. * Better path handling when recursively checking directories ending with slash. ## 0.13.0 (2016-01-04) ### New features and improvements * Empty statements (semicolons without preceding statements) are reported (#44). * Inline option `luacheck: push` can be followed by other options on the same line, e.g. `luacheck: push ignore`. * Better syntax error messages. * When recursively checking directories and `--include-files` is used, files are not filtered by `.lua` extension (#43). ### Fixes * Fixed crash when source ends with `.`, `"\` or `"\u{`. ## 0.12.0 (2015-11-02) ### New features and improvements * New `ngx_lua` globals set for Openresty ngx_lua module (#41). * Better CLI error messages. ### Fixes * Fixed duplicate `uninitialized access` and `unreachable code` warnings in nested functions. ### Miscellaneous * RTD theme is no longer required when building docs. * HTML docs are no longer stored in the repo. ## 0.11.1 (2015-08-09) ### Improvements * More accurate analysis around literal conditions, e.g. `while true do ... end`. * Extra threads are not created when number of files is less than value of `--jobs` option. ### Fixes * Fixed crash on unreachable repeat condition (#36). * Fixed crash when using `--ranges` with cache. * Fixed incorrect output or crashes when loading cache created by a different version (#37). * Fixed crash when an upvalue is followed by an infinite loop. ## 0.11.0 (2015-07-18) ### Breaking changes * Removed `--no-unused-globals` option, use `--ignore 13` instead. * Removed `.vararg` field for warnings related to varargs, check `.name == "..."` instead. * Errors now also have codes, starting with `0`, and are returned together with warnings from `luacheck.*` functions (#31). ### New features and improvements * During config lookup all directories starting from current one and up to file system root are traversed in search of config. Path-related data from config loaded from an upper directory is adjusted to work as if Luacheck was started from the directory with config (#20). * New `--exclude-files` and `--include-files` options for file filtering using globbing patterns (#21). * More CLI and config options can be used inline. * Underscores in inline option names can be replaced with spaces. * Inline options without arguments can be prefixed with `no` to invert meaning. * New built-in global set `busted` containing globals of Busted testing framework. * Stable interface for editor plugins. * New `luacheck.get_message` function for formatting a message for a warning or error. * Sets of standard globals can be merged using `+`. * If value of `std` option starts with `+`, new set is added to the old one instead of overwriting it, * New `--filename` option allows using real file name for picking config per-path overrides while passing source through stdin or a temporary file. * New `--ranges` option provides column ranges for tokens related to warnings (#32). * New `--no-self` option for ignoring warnings related to implicit `self` argument. * Config options can now be returned as a table. * Config now has access to all regular globals in its environment. * New sets of standard globals can be created by mutating global `stds` in config. * `formatter` config option now accepts functions. * Warnings returned from `luacheck.*` functions now have = `.end_column` field with last column of related token. * JUnit formatter now produces a testcase per each issue. ### Fixes * Fixed validation error when attempting to use `formatter` option in config. * Fixed incorrect error location for `invalid escape sequence` syntax errors. * FIxed spurious quotes in typecheck error messages in `luacheck.*` functions. * UTF BOM is now stripped when reading files. ## 0.10.0 (2015-03-13) ### Breaking changes * Removed `--limit`/`-l` option, use inline options to ignore warnings that are OK. * Removed `--no-unused-values`/`-v` option, use `--no-unused-secondaries`/`-s` instead. * Removed `--no-unset` option, use `--ignore 221` instead. ### New features and improvements * Added caching of check results (`--cache` and `--no-cache` options). * Added parallel checking (`--jobs`/`-j` option). * Added reporting of syntax error message and location in CLI (#17). * Added `--version` command for showing versions of Luacheck and its dependencies. * Added more functions to `luacheck` Lua module. ### Fixes * Fixed file status label not being colored when using `-qq`. ### Miscellaneous * Added installer script (`install.lua`). ## 0.9.0 (2015-02-15) ### New features and improvements * Added inline options: a way to precisely configure luacheck using inline comments of special format (#16). * Added an option to use custom output formatters; TAP and JUnit formatters are built-in (#19). ### Fixes * Fixed crash when checking stdin using a config with overrides. ## 0.8.0 (2015-01-19) ### New features and improvements * Added detection of unused labels. * Added detection of unreachable code. * Added detection of loops that can be executed at most once. * Added detection of uninitialized variables. * Added detection of shadowed local variables. * Added detection of empty blocks. * Added detection of unbalanced assignments. * New warning categorization system using warning codes. * Added possibility to mark globals as read-only (most standard globals are so by default). * Added possibility to overwrite options on per-directory basis in config. * Some CLI-specific options can now be used in config (e.g. `color`). * Added standard global sets for Lua 5.3. ### Miscellaneous * Removed unnecessary dependencies. * Simplified manual installation (#12). * Added executable wrapper for Windows (#14). ## 0.7.3 (2015-01-05) ### Fixes * Fixed false `unused variable` and `unused value` warnings when a closure accessing a variable is created in a nested block (#10). ## 0.7.2 (2015-01-03) ### Improvements * Improved analysis quality w.r.t unused values using flow-sensitive analysis. ## 0.7.1 (2014-12-16) ### Improvements * When `--no-color` is used, identifiers are now quoted in warning messages (#8). ### Fixes * Fixed priority of options: CLI options override config per-file overrides, which override general config. * Fixed ignoring `std` option in CLI when `compat` option is used in config. ## 0.7.0 (2014-11-23) ### New features and improvements * Added `--allow-defined-top` and `--module` options for more flexible checking of files which are supposed to set globals (#7). * Added `--no-unused-secondaries` option for removing warnings about unused values set together with used ones. * Added detection of variables that are used but never set. ### Fixes * Fixed ignoring `std` config option. * Fixed incompatibility with Lua 5.3. ## 0.6.0 (2014-11-01) ### New features and improvements * Luacheck can now check programs which use syntax introduced in Lua 5.2, Lua 5.3 and LuaJIT 2.0. * Luacheck is now faster. * Luacheck now exits with an error if it couldn't load a config due to an I/O, syntax, runtime or validation error. ### Miscellaneous * Removed dependency on MetaLua parser. ## 0.5.0 (2014-09-06) ### Breaking changes * Changed the interface of `luacheck` module. * Changed what `-qq` and `-qqq` do. ### New features and improvements * Added an option to disable colourization of output (#2). * Added an option to allow implicit global variable definition. * Filter out warnings about redefined `_` (#5). * `--globals`, `--ignore` and `--only` can now be used several times. * Passing `-` as an argument now checks stdin. * Passing a directory as an argument checks all `.lua` files inside it. * Added config loading (#1). * Added `--std` option, adding globals via `--globals` now does not require passing a dash. * Added `--new-globals` option. ## 0.4.1 (2014-08-25) ### Miscellaneous * Updated to argparse 0.3.0 ## 0.4.0 (2014-05-31) ### New features and improvements * Unused values (e.g. `local a = expr1; ... a = expr2`) are now detected. * In CLI, rockspecs (arguments ending with `.rockspec`) now expand into list of related `.lua` files. * Unused varargs are now detected. ## 0.3.0 (2014-04-25) ### New features and improvements * Luacheck is now _ENV-aware: "globals" inside chunks with custom `_ENV` are ignored, but their presence marks the `_ENV` variable as used; accessing the outermost ("default") `_ENV` is permitted, too. * In `--globals` option of the CLI hyphen now expands to all standard global variables. * New `-c`/`--compat` flag defines some additional globals for Lua 5.1/5.2 compatibility (e.g. `setfenv`). * New `-l`/`--limit` option allows setting a limit of warnings. If the limit is not exceeded, the CLI exits with `0`. * The `-q`/`--quiet` flag now can be used several times (`-q`/`-qq`/`-qqq`) to make the CLI more or less quiet. ## 0.2.0 (2014-04-05) ### New features and improvements * Command-line interface now prints per-file reports as they are produced instead of waiting for all files to be checked. * Luacheck now recognizes different types of variables (normal locals, function arguments and loop variables) and reports them accordingly. * Luacheck now distinguishes accessing global variables from setting them. * In command-line interface `-q` switch makes luacheck only print total number of warnings instead of suppressing output completely. ## 0.1.0 (2014-03-25) The first release. luacheck-0.21.1/LICENSE000066400000000000000000000021031315524664100143620ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 - 2017 Peter Melnichenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. luacheck-0.21.1/README.md000066400000000000000000000176751315524664100146600ustar00rootroot00000000000000# Luacheck [![Join the chat at https://gitter.im/luacheck/Lobby](https://badges.gitter.im/luacheck/Lobby.svg)](https://gitter.im/luacheck/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/mpeterv/luacheck.png?branch=master)](https://travis-ci.org/mpeterv/luacheck) [![Windows build status](https://ci.appveyor.com/api/projects/status/pgox2vvelagw1fux/branch/master?svg=true&passingText=Windows%20build%20passing&failingText=Windows%20build%20failing)](https://ci.appveyor.com/project/mpeterv/luacheck/branch/master) [![Coverage Status](https://coveralls.io/repos/mpeterv/luacheck/badge.svg?branch=master)](https://coveralls.io/r/mpeterv/luacheck?branch=master) [![License](https://img.shields.io/badge/License-MIT-brightgreen.svg)](LICENSE) ## Contents * [Overview](#overview) * [Installation](#installation) * [Basic usage](#basic-usage) * [Related projects](#related-projects) * [Documentation](#documentation) * [Development](#development) * [Building and testing](#building-and-testing) * [License](#license) ## Overview Luacheck is a static analyzer and a linter for [Lua](http://www.lua.org). Luacheck detects various issues such as usage of undefined global variables, unused variables and values, accessing uninitialized variables, unreachable code and more. Most aspects of checking are configurable: there are options for defining custom project-related globals, for selecting set of standard globals (version of Lua standard library), for filtering warnings by type and name of related variable, etc. The options can be used on the command line, put into a config or directly into checked files as Lua comments. Luacheck supports checking Lua files using syntax of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT. Luacheck itself is written in Lua and runs on all of mentioned Lua versions. ## Installation The easiest way to install Luacheck is to use [LuaRocks](https://luarocks.org/). From your command line run the following command (using `sudo` if necessary): ``` 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. ### Binary download For Windows there is experimental 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.21.1/luacheck.exe). ### Manual installation For manual installation, only a Lua interpreter binary is required. 1. Download and unpack latest Luacheck release ([.zip](https://github.com/mpeterv/luacheck/archive/0.21.1.zip), [.tar.gz](https://github.com/mpeterv/luacheck/archive/0.21.1.tar.gz)). 2. Run `install.lua ` script using the Lua interpreter. If Lua interpreter is not in `PATH`, invoke it using absolute path. 3. Add `/bin` to PATH or run Luacheck as `/bin/luacheck`. ## Basic usage After Luacheck is installed, run `luacheck` program from the command line. Pass a list of files, [rockspecs](https://github.com/keplerproject/luarocks/wiki/Rockspec-format) or directories (requires LuaFileSystem) to be checked: ``` luacheck src extra_file.lua another_file.lua ``` ``` Checking src/good_code.lua OK Checking src/bad_code.lua 3 warnings src/bad_code.lua:3:23: unused variable length argument src/bad_code.lua:7:10: setting non-standard global variable embrace src/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 Checking src/python_code.lua 1 error src/python_code.lua:1:6: expected '=' near '__future__' Checking extra_file.lua 5 warnings extra_file.lua:3:18: unused argument baz extra_file.lua:4:8: unused loop variable i extra_file.lua:13:7: accessing uninitialized variable a extra_file.lua:14:1: value assigned to variable x is unused extra_file.lua:21:7: variable z is never accessed Checking another_file.lua 2 warnings another_file.lua:2:7: unused variable height another_file.lua:3:7: accessing undefined variable heigth Total: 10 warnings / 1 error in 5 files ``` For more info, see [documentation](#documentation). ## Related projects ### Editor support There are a few plugins which allow using Luacheck directly inside an editor, showing warnings inline: * For Vim, [Syntastic](https://github.com/scrooloose/syntastic/) contains [luacheck checker](https://github.com/scrooloose/syntastic/wiki/Lua%3A---luacheck); * For Sublime Text 3 there is [SublimeLinter-luacheck](https://packagecontrol.io/packages/SublimeLinter-luacheck) which requires [SublimeLinter](http://sublimelinter.readthedocs.org/en/latest/); * For Atom there is [linter-luacheck](https://atom.io/packages/linter-luacheck) which requires [AtomLinter](https://github.com/atom-community/linter); * For Emacs, [Flycheck](http://www.flycheck.org/) contains [luacheck checker](http://www.flycheck.org/en/latest/languages.html#lua); * For Brackets, there is [linter.luacheck](https://github.com/Malcolm3141/brackets-luacheck) extension. * For Visual Studio code there is [vscode-luacheck](https://marketplace.visualstudio.com/items?itemName=dwenegar.vscode-luacheck) extension. If you are a plugin developer, see [recommended way of using Luacheck in a plugin](http://luacheck.readthedocs.org/en/stable/cli.html#stable-interface-for-editor-plugins-and-tools). ### Other projects * [Luacheck bindings for Node.js](https://www.npmjs.com/package/luacheck); * [Luacheck plugin for Gulp](https://www.npmjs.com/package/gulp-luacheck). ## Documentation Documentation is available [online](http://luacheck.readthedocs.org). If Luacheck has been installed using LuaRocks, it can be browsed offline using `luarocks doc luacheck` command. Documentation can be built using [Sphinx](http://sphinx-doc.org/): `sphinx-build docsrc doc`, the files will be found inside `doc/`. ## Development Luacheck is currently in development. The latest released version is 0.21.1. The interface of the `luacheck` module may change between minor releases. The command line interface is fairly stable. Use the Luacheck issue tracker on GitHub to submit bugs, suggestions and questions. Any pull requests are welcome, too. ## Building and testing After the Luacheck repo is cloned and changes are made, run `luarocks make` (optionally prepended with `sudo`) from its root directory to install dev version of Luacheck. To 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`. ## License ``` The MIT License (MIT) Copyright (c) 2014 - 2017 Peter Melnichenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` luacheck-0.21.1/appveyor.yml000066400000000000000000000005751315524664100157600ustar00rootroot00000000000000version: 1.0.{build} shallow_clone: true environment: matrix: - LUA: "lua 5.1" - LUA: "lua 5.2" - LUA: "lua 5.3" - LUA: "luajit 2.0" - LUA: "luajit 2.1" before_build: - set PATH=%CD%\here\bin;C:\Python27\Scripts;%PATH% - pip install hererocks build_script: - hererocks here --%LUA% -r latest - luarocks make - luarocks install busted test_script: busted luacheck-0.21.1/bin/000077500000000000000000000000001315524664100141315ustar00rootroot00000000000000luacheck-0.21.1/bin/luacheck.bat000066400000000000000000000000521315524664100163750ustar00rootroot00000000000000@echo off lua.exe "%~dp0\luacheck.lua" %* luacheck-0.21.1/bin/luacheck.lua000077500000000000000000000000531315524664100164140ustar00rootroot00000000000000#!/usr/bin/env lua require "luacheck.main" luacheck-0.21.1/docsrc/000077500000000000000000000000001315524664100146365ustar00rootroot00000000000000luacheck-0.21.1/docsrc/cli.rst000066400000000000000000000373541315524664100161530ustar00rootroot00000000000000Command line interface ====================== ``luacheck`` program accepts files, directories and `rockspecs `_ as arguments. They can be filtered using ``--include-files`` and ``--exclude-files`` options, see below. * Given a file, ``luacheck`` will check it. * Given ``-``, ``luacheck`` will check stdin. * Given a directory, ``luacheck`` will check all files within it, selecting only files with ``.lua`` extension unless ``--include-files`` option is used. This feature requires `LuaFileSystem `_ (installed automatically if LuaRocks was used to install Luacheck). * Given a rockspec (a file with ``.rockspec`` extension), ``luacheck`` will check all files with ``.lua`` extension mentioned in the rockspec in ``build.install.lua``, ``build.install.bin`` and ``build.modules`` tables. The output of ``luacheck`` consists of separate reports for each checked file and ends with a summary:: $ luacheck src Checking src/bad_code.lua 5 warnings src/bad_code.lua:3:16: unused variable helper src/bad_code.lua:3:23: unused variable length argument src/bad_code.lua:7:10: setting non-standard global variable embrace src/bad_code.lua:8:10: variable opt was previously defined as an argument on line 7 src/bad_code.lua:9:11: accessing undefined variable hepler Checking src/good_code.lua OK Checking src/python_code.lua 1 error src/python_code.lua:1:6: expected '=' near '__future__' Checking src/unused_code.lua 9 warnings src/unused_code.lua:3:18: unused argument baz src/unused_code.lua:4:8: unused loop variable i src/unused_code.lua:5:13: unused variable q src/unused_code.lua:7:11: unused loop variable a src/unused_code.lua:7:14: unused loop variable b src/unused_code.lua:7:17: unused loop variable c src/unused_code.lua:13:7: value assigned to variable x is unused src/unused_code.lua:14:1: value assigned to variable x is unused src/unused_code.lua:22:1: value assigned to variable z is unused Total: 14 warnings / 1 error in 4 files ``luacheck`` chooses exit code as follows: * Exit code is ``0`` if no warnings or errors occurred. * Exit code is ``1`` if some warnings occured but there were no syntax errors or invalid inline options. * Exit code is ``2`` if there were some syntax errors or invalid inline options. * Exit code is ``3`` if some files couldn't be checked, typically due to an incorrect file name. * Exit code is ``4`` if there was a critical error (invalid CLI arguments, config, or cache file). .. _cliopts: Command line options -------------------- Short options that do not take an argument can be combined into one, so that ``-qqu`` is equivalent to ``-q -q -u``. For long options, both ``--option value`` or ``--option=value`` can be used. Options taking several arguments can be used several times; ``--ignore foo --ignore bar`` is equivalent to ``--ignore foo bar``. Note that options that may take several arguments, such as ``--globals``, should not be used immediately before positional arguments; given ``--globals foo bar file.lua``, ``luacheck`` will consider all ``foo``, ``bar`` and ``file.lua`` global and then panic as there are no file names left. ======================================= ================================================================================ Option Meaning ======================================= ================================================================================ ``-g | --no-global`` Filter out warnings related to global variables. ``-u | --no-unused`` Filter out warnings related to unused variables and values. ``-r | --no-redefined`` Filter out warnings related to redefined variables. ``-a | --no-unused-args`` Filter out warnings related to unused arguments and loop variables. ``-s | --no-unused-secondaries`` Filter out warnings related to unused variables set together with used ones. See :ref:`secondaryvaluesandvariables` ``--no-self`` Filter out warnings related to implicit ``self`` argument. ``--std `` Set standard globals. ```` can be one of: * ``lua51`` - globals of Lua 5.1 without deprecated ones; * ``lua51c`` - globals of Lua 5.1; * ``lua52`` - globals of Lua 5.2; * ``lua52c`` - globals of Lua 5.2 compiled with LUA_COMPAT_ALL; * ``lua53`` - globals of Lua 5.3; * ``lua53c`` - globals of Lua 5.3 compiled with LUA_COMPAT_5_2; * ``luajit`` - globals of LuaJIT 2.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; * ``none`` - no standard globals. See :ref:`stds` ``--globals [] ...`` Add custom global variables or fields on top of standard ones. See :ref:`fields` ``--read-globals [] ...`` Add read-only global variables or fields. ``--new-globals [] ...`` Set custom global variables or fields. Removes custom globals added previously. ``--new-read-globals [] ...`` Set read-only global variables or fields. Removes read-only globals added previously. ``--not-globals [] ...`` Remove custom and standard global variables or fields. ``-c | --compat`` Equivalent to ``--std max``. ``-d | --allow-defined`` Allow defining globals implicitly by setting them. See :ref:`implicitlydefinedglobals` ``-t | --allow-defined-top`` Allow defining globals implicitly by setting them in the top level scope. See :ref:`implicitlydefinedglobals` ``-m | --module`` Limit visibility of implicitly defined globals to their files. See :ref:`modules` ``--max-line-length [] ...`` 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. Default global location is: * ``%LOCALAPPDATA%\Luacheck\.luacheckrc`` on Windows; * ``~/Library/Application Support/Luacheck/.luacheckrc`` on OS X/macOS; * ``$XDG_CONFIG_HOME/luacheck/.luacheckrc`` or ``~/.config/luacheck/.luacheckrc`` on other systems. ``--no-default-config`` Do not use fallback configuration file. ``--filename `` Use another filename in output, for selecting configuration overrides and for file filtering. ``--exclude-files [] ...`` Do not check files matching these globbing patterns. Recursive globs such as ``**/*.lua`` are supported. ``--include-files [] ...`` Do not check files not matching these globbing patterns. ``--cache []`` Path to cache file. (default: ``.luacheckcache``). See :ref:`cache` ``--no-cache`` Do not use cache. ``-j | --jobs`` Check ```` files in parallel. Requires `LuaLanes `_. Default number of jobs is set to number of available processing units. ``--formatter `` Use custom formatter. ```` must be a module name or one of: * ``TAP`` - Test Anything Protocol formatter; * ``JUnit`` - JUnit XML formatter; * ``plain`` - simple warning-per-line formatter; * ``default`` - standard formatter. ``-q | --quiet`` Suppress report output for files without warnings. * ``-qq`` - Suppress output of warnings. * ``-qqq`` - Only output summary. ``--codes`` Show warning codes. ``--ranges`` Show ranges of columns related to warnings. ``--no-color`` Do not colorize output. ``-v | --version`` Show version of Luacheck and its dependencies and exit. ``-h | --help`` Show help and exit. ======================================= ================================================================================ .. _patterns: Patterns -------- CLI options ``--ignore``, ``--enable`` and ``--only`` and corresponding config options allow filtering warnings using pattern matching on warning codes, variable names or both. If a pattern contains a slash, the part before slash matches warning code and the part after matches variable name. Otherwise, if a pattern contains a letter or underscore, it matches variable name. Otherwise, it matches warning code. E.g.: ======= ========================================================================= Pattern Matching warnings ======= ========================================================================= 4.2 Shadowing declarations of arguments or redefining them. .*_ Warnings related to variables with ``_`` suffix. 4.2/.*_ Shadowing declarations of arguments with ``_`` suffix or redefining them. ======= ========================================================================= Unless already anchored, patterns matching variable names are anchored at both sides and patterns matching warning codes are anchored at their beginnings. This allows one to filter warnings by category (e.g. ``--only 1`` focuses ``luacheck`` on global-related warnings). .. _fields: Defining extra globals and fields --------------------------------- CLI options ``--globals``, ``--new-globals``, ``--read-globals``, ``--new-read-globals``, and corresponding config options add new allowed globals or fields. E.g. ``--read-globals foo --globals foo.bar`` allows accessing ``foo`` global and mutating its ``bar`` field. ``--not-globals`` also operates on globals and fields and removes definitions of both standard and custom globals. .. _stds: 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 default one instead of replacing it. For instance, ``--std +busted`` is suitable for checking test files that use `Busted `_ testing framework. Custom sets of globals can be defined by mutating global variable ``stds`` in config. See :ref:`custom_stds` Formatters ---------- CLI option ``--formatter`` allows selecting a custom formatter for ``luacheck`` output. A custom formatter is a Lua module returning a function with three arguments: report as returned by ``luacheck`` module (see :ref:`report`), array of file names and table of options. Options contain values assigned to ``quiet``, ``color``, ``limit``, ``codes``, ``ranges`` and ``formatter`` options in CLI or config. Formatter function must return a string. .. _cache: Caching ------- If LuaFileSystem is available, Luacheck can cache results of checking files. On subsequent checks, only files which have changed since the last check will be rechecked, improving run time significantly. Changing options (e.g. defining additional globals) does not invalidate cache. Caching can be enabled by using ``--cache `` option or ``cache`` config option. Using ``--cache`` without an argument or setting ``cache`` config option to ``true`` sets ``.luacheckcache`` as the cache file. Note that ``--cache`` must be used every time ``luacheck`` is run, not on the first run only. Stable interface for editor plugins and tools --------------------------------------------- Command-line interface of Luacheck can change between minor releases. Starting from 0.11.0 version, the following interface is guaranteed at least till 1.0.0 version, and should be used by tools using Luacheck output, e.g. editor plugins. * Luacheck should be started from the directory containing the checked file. * File can be passed through stdin using ``-`` as argument or using a temporary file. Real filename should be passed using ``--filename`` option. * Plain formatter should be used. It outputs one issue (warning or error) per line. * To get precise error location, ``--ranges`` option can be used. Each line starts with real filename (passed using ``--filename``), followed by ``::-:``, where ```` is line number on which issue occurred and ``-`` is inclusive range of columns of token related to issue. Numbering starts from 1. If ``--ranges`` is not used, end column and dash is not printed. * To get warning and error codes, ``--codes`` option can be used. For each line, substring between parentheses contains three digit issue code, prefixed with ``E`` for errors and ``W`` for warnings. Lack of such substring indicates a fatal error (e.g. I/O error). * The rest of the line is warning message. If compatibility with older Luacheck version is desired, output of ``luacheck --help`` can be used to get its version. If it contains string ``0..``, where ```` is at least 11 and ``patch`` is any number, interface described above should be used. luacheck-0.21.1/docsrc/conf.py000066400000000000000000000202751315524664100161430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # luacheck documentation build configuration file, created by # sphinx-quickstart on Thu Sep 4 16:25:37 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'luacheck' copyright = u'2014 - 2017, Peter Melnichenko' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.21.1' # The full version, including alpha/beta/rc tags. release = '0.21.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import os if os.environ.get('READTHEDOCS', None) != 'True': try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: pass # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'luacheckdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'luacheck.tex', u'luacheck Documentation', u'Peter Melnichenko', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'luacheck', u'luacheck Documentation', [u'Peter Melnichenko'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'luacheck', u'luacheck Documentation', u'Peter Melnichenko', 'luacheck', 'A simple Lua static analyzer.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False luacheck-0.21.1/docsrc/config.rst000066400000000000000000000175621315524664100166500ustar00rootroot00000000000000Configuration file ================== ``luacheck`` tries to load configuration from ``.luacheckrc`` file in the current directory. If not found, it will look for it in the parent directory and so on, going up until it reaches file system root. Path to config can be set using ``--config`` option, in which case it will be used during recursive loading. Config loading can be disabled using ``--no-config`` flag. If neither of ``--config``, ``--no-config``, and ``--no-default-config`` options are used, ``luacheck`` will attempt to load configuration from value of ``--default-config`` option, or ``%LOCALAPPDATA%\Luacheck\.luacheckrc`` on Windows, ``~/Library/Application Support/Luacheck/.luacheckrc`` on OS X/macOS, and ``$XDG_CONFIG_HOME/luacheck/.luacheckrc`` or ``~/.config/luacheck/.luacheckrc`` on other systems by default. Config is simply a Lua script executed by ``luacheck``. It may set various options by assigning to globals or by returning a table with option names as keys. Options loaded from config have the lowest priority: it's possible to overwrite them with CLI options or inline options. .. _options: Config options -------------- =========================== ======================================== =================== Option Type Default value =========================== ======================================== =================== ``color`` Boolean ``true`` ``codes`` Boolean ``false`` ``formatter`` String or function ``"default"`` ``cache`` Boolean or string ``false`` ``jobs`` Positive integer ``1`` ``exclude_files`` Array of strings ``{}`` ``include_files`` Array of strings (Include all files) ``global`` Boolean ``true`` ``unused`` Boolean ``true`` ``redefined`` Boolean ``true`` ``unused_args`` Boolean ``true`` ``unused_secondaries`` Boolean ``true`` ``self`` Boolean ``true`` ``std`` String or set of standard globals ``"_G"`` ``globals`` Array of strings 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 ``{}`` ``new_read_globals`` Array of strings or field definition map (Do not overwrite) ``not_globals`` Array of strings ``{}`` ``compat`` Boolean ``false`` ``allow_defined`` Boolean ``false`` ``allow_defined_top`` Boolean ``false`` ``module`` Boolean ``false`` ``max_line_length`` Number or ``false`` ``120`` ``max_code_line_length`` Number or ``false`` ``120`` ``max_string_line_length`` Number or ``false`` ``120`` ``max_comment_line_length`` Number or ``false`` ``120`` ``ignore`` Array of patterns (see :ref:`patterns`) ``{}`` ``enable`` Array of patterns ``{}`` ``only`` Array of patterns (Do not filter) ``inline`` Boolean ``true`` =========================== ======================================== =================== An example of a config which makes ``luacheck`` ensure that only globals from the portable intersection of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0 are used, as well as disables detection of unused arguments: .. code-block:: lua :linenos: std = "min" ignore = {"212"} .. _custom_stds: Custom sets of globals ---------------------- ``std`` option allows setting a custom standard set of globals using a table. This table can have two fields: ``globals`` and ``read_globals``. Both of them should contain a field definition map defining some globals. The simplest way to define globals is to list their names: .. code-block:: lua :linenos: std = { globals = {"foo", "bar"}, -- these globals can be set and accessed. read_globals = {"baz", "quux"} -- these globals can only be accessed. } For globals defined like this Luacheck will additionally consider any fields within them defined. To define a global with a restricted set of fields, use global name as key and a table as value. In that table, ``fields`` subtable can contain the fields in the same format: .. code-block:: lua :linenos: std = { read_globals = { foo = { -- Defining read-only global `foo`... fields = { field1 = { -- `foo.field1` is now defined... fields = { nested_field = {} -- `foo.field1.nested_field` is now defined... } }, field2 = {} -- `foo.field2` is defined. } } } } Globals and fields can be marked read-only or not using ``read_only`` property with a boolean value. Property ``other_fields`` controls whether the global or field can also contain other unspecified fields: .. code-block:: lua :linenos: std = { read_globals = { foo = { -- `foo` and its fields are read-only by default (because they are within `read_globals` table). fields = { bar = { read_only = false, -- `foo.bar` is not read-only, can be set. other_fields = true, -- `foo.bar[anything]` is defined and can be set or mutated (inherited from `foo.bar`). fields = { baz = {read_only = true}, -- `foo.bar.baz` is read-only as an exception. } } } } } } Custom sets can be given names by mutating global ``stds`` variable, so that they can then be used in ``--std`` CLI option and ``std`` inline and config option. .. code-block:: lua :linenos: stds.some_lib = {...} std = "min+some_lib" In config, ``globals``, ``new_globals``, ``read_globals``, and ``new_read_globals`` can also contain definitions in same format: .. code-block:: lua :linenos: read_globals = { server = { fields = { -- Allow mutating `server.sessions` with any keys... sessions = {read_only = false, other_fields = true}, -- other fields... } }, --- other globals... } 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/``: .. code-block:: lua :linenos: std = "min" 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 .. code-block:: lua files["src/dir"].enable = {"212"} and .. code-block:: lua files["src/dir"] = {enable = {"212"}} are equivalent. luacheck-0.21.1/docsrc/index.rst000066400000000000000000000004051315524664100164760ustar00rootroot00000000000000Luacheck documentation ====================== Contents: .. toctree:: warnings cli config inline module This is documentation for 0.21.1 version of `Luacheck `_, a linter for `Lua `_. luacheck-0.21.1/docsrc/inline.rst000066400000000000000000000056641315524664100166610ustar00rootroot00000000000000Inline options ============== 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: ======================= ==================================================================== Option Number of arguments ======================= ==================================================================== global 0 unused 0 redefined 0 unused args 0 unused secondaries 0 self 0 compat 0 module 0 allow defined 0 allow defined top 0 max line length 1 (with ``no`` and no arguments disables line length checks) max code line length 1 (with ``no`` and no arguments disables code line length checks) max string line length 1 (with ``no`` and no arguments disables string line length checks) max comment line length 1 (with ``no`` and no arguments disables comment line length checks) std 1 globals 0+ new globals 0+ read globals 0+ new read globals 0+ not globals 0+ ignore 0+ (without arguments everything is ignored) enable 1+ only 1+ ======================= ==================================================================== Options that take no arguments can be prefixed with ``no`` to invert their meaning. E.g. ``--luacheck: no unused args`` disables unused argument warnings. Part of the file affected by inline option dependes on where it is placed. If there is any code on the line with the option, only that line is affected; otherwise, everything till the end of the current closure is. In particular, inline options at the top of the file affect all of it: .. code-block:: lua :linenos: -- luacheck: globals g1 g2, ignore foo local foo = g1(g2) -- No warnings emitted. -- The following unused function is not reported. local function f() -- luacheck: ignore -- luacheck: globals g3 g3() -- No warning. end g3() -- Warning is emitted as the inline option defining g3 only affected function f. For fine-grained control over inline option visibility use ``luacheck: push`` and ``luacheck: pop`` directives: .. code-block:: lua :linenos: -- luacheck: push ignore foo foo() -- No warning. -- luacheck: pop foo() -- Warning is emitted. Inline options can be completely disabled using ``--no-inline`` CLI option or ``inline`` config option. luacheck-0.21.1/docsrc/module.rst000066400000000000000000000066721315524664100166700ustar00rootroot00000000000000Luacheck module =============== Use ``local luacheck = require "luacheck"`` to import ``luacheck`` module. It contains the following functions: * ``luacheck.get_report(source)``: Given source string, returns analysis data (a table). * ``luacheck.process_reports(reports, options)``: Processes array of analysis reports and applies options. ``reports[i]`` uses ``options``, ``options[i]``, ``options[i][1]``, ``options[i][2]``, ... as options, overriding each other in that order. Options table is a table with fields similar to config options; see :ref:`options`. Analysis reports with field ``fatal`` are ignored. ``process_reports`` returns final report, see :ref:`report`. * ``luacheck.check_strings(sources, options)``: Checks array of sources using options, returns final report. Tables with field ``fatal`` within ``sources`` array are ignored. * ``luacheck.check_files(files, options)``: Checks array of files using options, returns final report. Open file handles can passed instead of filenames, in which case they will be read till EOF and closed. * ``luacheck.get_message(issue)``: Returns a string message for an issue, see :ref:`report`. ``luacheck._VERSION`` contains Luacheck version as a string in ``MAJOR.MINOR.PATCH`` format. Using ``luacheck`` as a function is equivalent to calling ``luacheck.check_files``. .. _report: Report format ------------- A final report is an array of file reports plus fields ``warnings``, ``errors`` and ``fatals`` containing total number of warnings, errors and fatal errors, correspondingly. A file report is an array of issues (warnings or errors). If a fatal error occurred while checking a file, its report will have ``fatal`` field containing error type and ``msg`` field containing error message. An issue is a table with field ``code`` indicating its type (see :doc:`warnings`), and fields ``line``, ``column`` and ``end_column`` pointing to the source of the warning. ``name`` field may contain name of related variable. Issues of some types can also have additional fields: ============= ============================================================================================================ Codes Additional fields ============= ============================================================================================================ 011 ``msg`` field contains syntax error message. 111 ``module`` field indicates that assignment is to a non-module global variable. 122, 142, 143 ``indirect`` field indicates that the global field was accessed using a local alias. 122, 142, 143 ``field`` field contains string representation of related global field. 211 ``func`` field indicates that unused variable is a function. 211 ``recursive`` field indicates that unused function is recursive. 211 ``mutually_recursive`` field is set for unused mutually recursive functions. 314 ``field`` field contains string representation of ununsed field or index. 4.. ``prev_line`` and ``prev_column`` fields contain location of the overwritten definition. 521 ``label`` field contains label name. 631 ``line_ending`` field contains ``"comment"`` or ``"string"`` if line ending is within a comment or a string. 631 ``max_length`` field contains maximum allowed line length. ============= ============================================================================================================ Other fields may be present for internal reasons. luacheck-0.21.1/docsrc/warnings.rst000066400000000000000000000234201315524664100172210ustar00rootroot00000000000000List of warnings ================ Warnings produced by Luacheck are categorized using three-digit warning codes. Warning codes can be displayed in CLI output using ``--codes`` CLI option or ``codes`` config option. Errors also have codes starting with zero; unlike warnings, they can not be ignored. ==== ================================================================= Code Description ==== ================================================================= 011 A syntax error. 021 An invalid inline option. 022 An unpaired inline push directive. 023 An unpaired inline pop directive. 111 Setting an undefined global variable. 112 Mutating an undefined global variable. 113 Accessing an undefined global variable. 121 Setting a read-only global variable. 122 Setting a read-only field of a global variable. 131 Unused implicitly defined global variable. 142 Setting an undefined field of a global variable. 143 Accessing an undefined field of a global variable. 211 Unused local variable. 212 Unused argument. 213 Unused loop variable. 221 Local variable is accessed but never set. 231 Local variable is set but never accessed. 232 An argument is set but never accessed. 233 Loop variable is set but never accessed. 241 Local variable is mutated but never accessed. 311 Value assigned to a local variable is unused. 312 Value of an argument is unused. 313 Value of a loop variable is unused. 314 Value of a field in a table literal is unused. 321 Accessing uninitialized local variable. 331 Value assigned to a local variable is mutated but never accessed. 341 Mutating uninitialized local variable. 411 Redefining a local variable. 412 Redefining an argument. 413 Redefining a loop variable. 421 Shadowing a local variable. 422 Shadowing an argument. 423 Shadowing a loop variable. 431 Shadowing an upvalue. 432 Shadowing an upvalue argument. 433 Shadowing an upvalue loop variable. 511 Unreachable code. 512 Loop can be executed at most once. 521 Unused label. 531 Left-hand side of an assignment is too short. 532 Left-hand side of an assignment is too long. 541 An empty ``do`` ``end`` block. 542 An empty ``if`` branch. 551 An empty statement. 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) ---------------------- For each file, Luacheck builds list of defined globals and fields which can be used there. By default only globals from Lua standard library are defined; custom globals can be added using ``--globals`` CLI option or ``globals`` config option, and version of standard library can be selected using ``--std`` CLI option or ``std`` config option. When an undefined global or field is set, mutated or accessed, Luacheck produces a warning. Read-only globals ^^^^^^^^^^^^^^^^^ By default, most standard globals and fields are marked as read-only, so that setting them produces a warning. Custom read-only globals and fields can be added using ``--read-globals`` CLI option or ``read_globals`` config option, or using a custom set of globals. See :ref:`custom_stds` Globals and fields that are not read-only by default: * ``_G`` * ``_ENV`` (treated as a global by Luacheck) * ``package.path`` * ``package.cpath`` * ``package.loaded`` * ``package.preload`` * ``package.loaders`` * ``package.searchers`` .. _implicitlydefinedglobals: Implicitly defined globals ^^^^^^^^^^^^^^^^^^^^^^^^^^ Luacheck can be configured to consider globals assigned under some conditions to be defined implicitly. When ``-d``/``--allow_defined`` CLI option or ``allow_defined`` config option is used, all assignments to globals define them; when ``-t``/``--allow_defined_top`` CLI option or ``allow_defined_top`` config option is used, assignments to globals in the top level function scope (also known as main chunk) define them. A warning is produced when an implicitly defined global is not accessed anywhere. .. _modules: Modules ^^^^^^^ Files can be marked as modules using ``-m``/``--module`` CLI option or ``module`` config option to simulate semantics of the deprecated `module `_ function. Globals implicitly defined inside a module are considired part of its interface, are not visible outside and are not reported as unused. Assignments to other globals are not allowed, even to defined ones. Unused variables (2xx) and values (3xx) --------------------------------------- Luacheck generates warnings for all unused local variables except one named ``_``. It also detects variables which are set but never accessed or accessed but never set. Unused values and uninitialized variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For each value assigned to a local variable, Luacheck computes set of expressions where it could be used. Warnings are produced for unused values (when a value can't be used anywhere) and for accessing uninitialized variables (when no values can reach an expression). E.g. in the following snippet value assigned to ``foo`` on line 1 is unused, and variable ``bar`` is uninitialized on line 9: .. code-block:: lua :linenos: local foo = expr1() local bar if condition() then foo = expr2() bar = expr3() else foo = expr4() print(bar) end return foo, bar .. _secondaryvaluesandvariables: Secondary values and variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused value assigned to a local variable is secondary if its origin is the last item on the RHS of assignment, and another value from that item is used. Secondary values typically appear when result of a function call is put into locals, and only some of them are later used. For example, here value assigned to ``b`` is secondary, value assigned to ``c`` is used, and value assigned to ``a`` is simply unused: .. code-block:: lua :linenos: local a, b, c = f(), g() return c A variable is secondary if all values assigned to it are secondary. In the snippet above, ``b`` is a secondary variable. Warnings related to unused secondary values and variables can be removed using ``-s``/``--no-unused-secondaries`` CLI option or ``unused_secondaries`` config option. Shadowing declarations (4xx) ---------------------------- Luacheck detects declarations of local variables shadowing previous declarations, unless the variable is named ``_``. If the previous declaration is in the same scope as the new one, it is called redefining. Note that it is **not** necessary to define a new local variable when overwriting an argument: .. code-block:: lua :linenos: local function f(x) local x = x or "default" -- bad end local function f(x) x = x or "default" -- good end Control flow and data flow issues (5xx) --------------------------------------- Unreachable code ^^^^^^^^^^^^^^^^ Luacheck detects unreachable code. It also detects it if end of a loop block is unreachable, which means that the loop can be executed at most once: .. code-block:: lua :linenos: for i = 1, 100 do -- Break statement is outside the `if` block, -- so that the loop always stops after the first iteration. if cond(i) then f() end break end Unused labels ^^^^^^^^^^^^^ Labels that are not used by any ``goto`` statements are reported as unused. Unbalanced assignments ^^^^^^^^^^^^^^^^^^^^^^ If an assignment has left side and right side with different lengths, the assignment is unbalanced and Luacheck warns about it. An exception is initializing several local variables in a single statement while leaving some uninitialized: .. code-block:: lua :linenos: local a, b, c = nil -- Effectively sets `a`, `b`, and `c` to nil, no warning. Empty blocks ^^^^^^^^^^^^ Luacheck warns about empty ``do`` ``end`` blocks and empty ``if`` branches (``then`` ``else``, ``then`` ``elseif``, and ``then`` ``end``). Empty statements ^^^^^^^^^^^^^^^^ In Lua 5.2+ semicolons are considered statements and can appear even when not following normal statements. Such semicolons produce Luacheck warnings as they are completely useless. Formatting issues (6xx) ----------------------- Whitespace issues ^^^^^^^^^^^^^^^^^ Luacheck warns about trailing whitespace and inconsistent indentation (``SPACE`` followed by ``TAB``). Some examples of trailing whitespace Luacheck finds: .. code-block:: lua :linenos: -- Whitespace example. print("Hello") print("World") Here: * Any tabs or spaces after either ``)`` would be considered trailing. * Any tabs or spaces after the ``.`` in the comment would be considered trailing * Any tabs or spaces on the empty line between the two ``print`` statements would also be considered a form of trailing whitespace. Trailing whitespace in any of these forms is useless, can be a nuisance to developers navigating around a file, and is forbidden in many formatting styles. Line length limits ^^^^^^^^^^^^^^^^^^ Luacheck warns about lines that are longer then some limit. Default limit is ``120`` characters. It's possible to change this limit using ``--max-line-length`` CLI option or disable the check completely with ``--no-max-line-length``; there are similar config and inline options. Additionally, separate limits can be set for three different type of lines: * "String" lines have their line ending inside a string, typically a long string using ``[[...]]`` syntax. * "Comment" lines have their line ending inside a long comment (``--[[...]]``), or end with a short comment using normal ``--...`` syntax. * "Code" lines are all other lines. These types of lines are limited using CLI options named ``--[no-]max-string-line-length``, ``--[no-]max-comment-line-length``, and ``--[no-]max-code-line-length``, with similar config and inline options. luacheck-0.21.1/install.lua000077500000000000000000000100461315524664100155360ustar00rootroot00000000000000#!/usr/bin/env lua local dirsep = package.config:sub(1, 1) local is_windows = dirsep == "\\" package.path = "src" .. dirsep .. "?.lua" local has_luacheck, luacheck = pcall(require, "luacheck.init") assert(has_luacheck, "couldn't find luacheck module") local has_argparse, argparse = pcall(require, "luacheck.argparse") assert(has_argparse, "couldn't find argparse module") local lua_executable = assert(arg[-1], "couldn't detect Lua executable") local parser = argparse(" install.lua", "Luacheck " .. luacheck._VERSION .. " installer.") parser:argument("path", ([[ Installation path. Luacheck executable scripts will be installed into %sbin. Luacheck modules will be installed into %ssrc. Pass . to build luacheck executable script without installing.]]):format(dirsep, dirsep)) parser:option("--lua", "Absolute path to lua interpreter or its name if it's in PATH.", lua_executable) local args = parser:parse() local function run_command(cmd) if is_windows then cmd = cmd .. " >NUL" else cmd = cmd .. " >/dev/null" end print(" Running " .. cmd) local ok = os.execute(cmd) assert(ok == true or ok == 0, "couldn't run " .. cmd) end local function mkdir(dir) if is_windows then run_command(([[if not exist "%s" md "%s"]]):format(dir, dir)) else run_command(([[mkdir -p "%s"]]):format(dir)) end end local function copy(src, dest) if is_windows then run_command(([[copy /y "%s" "%s"]]):format(src, dest)) else run_command(([[cp "%s" "%s"]]):format(src, dest)) end end print(("Installing luacheck %s into %s"):format(luacheck._VERSION, args.path)) print() local luacheck_executable = "bin" .. dirsep .. "luacheck" local luacheck_src_dir = args.path .. dirsep .. "src" local luacheck_lib_dir = luacheck_src_dir .. dirsep .. "luacheck" local luacheck_bin_dir = args.path .. dirsep .. "bin" if is_windows then print(" Detected Windows environment") luacheck_executable = luacheck_executable .. ".bat" else -- Close enough. print(" Detected POSIX environment") end print(" Writing luacheck executable to " .. luacheck_executable) local fh = assert(io.open(luacheck_executable, "wb"), "couldn't open " .. luacheck_executable) if is_windows then fh:write(([=[ @echo off "%s" -e "package.path=[[%%~dp0..\src\?.lua;%%~dp0..\src\?\init.lua;]]..package.path" "%%~dp0luacheck.lua" %%* ]=]):format(args.lua)) else fh:write(([=[ #!/bin/sh exec "%s" -e "package.path=[[%s/../src/?.lua;%s/../src/?/init.lua;]]..package.path" "%s/luacheck.lua" "$@" ]=]):format(args.lua, '$(dirname "$0")', '$(dirname "$0")', '$(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 { "main.lua", "init.lua", "config.lua", "linearize.lua", "analyze.lua", "reachability.lua", "core_utils.lua", "check.lua", "parser.lua", "lexer.lua", "filter.lua", "options.lua", "inline_options.lua", "builtin_standards.lua", "love_standard.lua", "ngx_standard.lua", "expand_rockspec.lua", "multithreading.lua", "cache.lua", "format.lua", "version.lua", "fs.lua", "globbing.lua", "utils.lua", "argparse.lua", "whitespace.lua", "detect_globals.lua", "standards.lua", "lua_fs.lua", "lfs_fs.lua"} do copy("src" .. dirsep .. "luacheck" .. dirsep .. filename, luacheck_lib_dir) end print(" Installing luacheck executables into " .. luacheck_bin_dir) mkdir(luacheck_bin_dir) copy(luacheck_executable, luacheck_bin_dir) copy("bin" .. dirsep .. "luacheck.lua", luacheck_bin_dir) print() print(("Installed luacheck %s into %s."):format(luacheck._VERSION, args.path)) print(("Please ensure that %s is in PATH."):format(luacheck_bin_dir)) luacheck-0.21.1/luacheck-scm-1.rockspec000066400000000000000000000047161315524664100176210ustar00rootroot00000000000000package = "luacheck" version = "scm-1" source = { url = "git+https://github.com/mpeterv/luacheck.git" } description = { summary = "A static analyzer and a linter for Lua", detailed = [[ Luacheck is a command-line tool for linting and static analysis of Lua code. It is able to spot usage of undefined global variables, unused local variables and a few other typical problems within Lua programs. ]], homepage = "https://github.com/mpeterv/luacheck", license = "MIT " } dependencies = { "lua >= 5.1, < 5.4", "luafilesystem >= 1.6.3" } build = { type = "builtin", modules = { luacheck = "src/luacheck/init.lua", ["luacheck.analyze"] = "src/luacheck/analyze.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.config"] = "src/luacheck/config.lua", ["luacheck.core_utils"] = "src/luacheck/core_utils.lua", ["luacheck.detect_globals"] = "src/luacheck/detect_globals.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.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.reachability"] = "src/luacheck/reachability.lua", ["luacheck.standards"] = "src/luacheck/standards.lua", ["luacheck.utils"] = "src/luacheck/utils.lua", ["luacheck.version"] = "src/luacheck/version.lua", ["luacheck.whitespace"] = "src/luacheck/whitespace.lua" }, install = { bin = { luacheck = "bin/luacheck.lua" } } } luacheck-0.21.1/scripts/000077500000000000000000000000001315524664100150505ustar00rootroot00000000000000luacheck-0.21.1/scripts/build-windows.sh000077500000000000000000000036441315524664100202050ustar00rootroot00000000000000#!/usr/bin/env bash set -eu set -o pipefail # Builds luacheck.exe (64bit) using MinGW and Luastatic. # Should be executed from root Luacheck directory. # Resulting binary will be in `build/luacheck.exe`. rm -rf build mkdir build cd build echo echo "=== Downloading Lua 5.3.4 ===" echo curl https://www.lua.org/ftp/lua-5.3.4.tar.gz | tar xz echo echo "=== Downloading LuaFileSystem 1.6.3-2 ===" echo luarocks unpack luafilesystem 1.6.3-2 echo echo "=== Downloading Lanes 3.10.1-1 ===" echo luarocks unpack lanes 3.10.1-1 echo echo "=== Building Lua 5.3.4 ===" echo cd lua-5.3.4 make mingw CC=x86_64-w64-mingw32-gcc AR="x86_64-w64-mingw32-ar rcu" cp src/liblua.a .. cd .. echo echo "=== Building LuaFileSystem 1.6.3-2 ===" echo cd luafilesystem-1.6.3-2/luafilesystem x86_64-w64-mingw32-gcc -c -O2 src/lfs.c -I../../lua-5.3.4/src -o src/lfs.o x86_64-w64-mingw32-ar rcs src/lfs.a src/lfs.o cp src/lfs.a ../.. cd ../.. echo echo "=== Building Lanes 3.10.1-1 ===" echo cd lanes-3.10.1-1/lanes x86_64-w64-mingw32-gcc -c -O2 src/compat.c -I../../lua-5.3.4/src -o src/compat.o x86_64-w64-mingw32-gcc -c -O2 src/deep.c -I../../lua-5.3.4/src -o src/deep.o x86_64-w64-mingw32-gcc -c -O2 src/lanes.c -I../../lua-5.3.4/src -o src/lanes.o x86_64-w64-mingw32-gcc -c -O2 src/keeper.c -I../../lua-5.3.4/src -o src/keeper.o x86_64-w64-mingw32-gcc -c -O2 src/tools.c -I../../lua-5.3.4/src -o src/tools.o x86_64-w64-mingw32-gcc -c -O2 src/threading.c -I../../lua-5.3.4/src -o src/threading.o x86_64-w64-mingw32-ar rcs src/lanes.a src/compat.o src/deep.o src/lanes.o src/keeper.o src/tools.o src/threading.o cp src/lanes.a ../.. cp src/lanes.lua ../.. cd ../.. echo echo "=== Copying Luacheck sources ===" echo cp -r ../src/luacheck . mkdir -p bin cp ../bin/luacheck.lua bin echo echo "=== Building luacheck.exe ===" echo CC="x86_64-w64-mingw32-gcc" luastatic bin/luacheck.lua luacheck/*.lua lanes.lua liblua.a lfs.a lanes.a -Ilua-5.3.4/src strip luacheck.exe cd .. luacheck-0.21.1/scripts/package.sh000077500000000000000000000014271315524664100170060ustar00rootroot00000000000000#!/usr/bin/env bash set -eu set -o pipefail # Creates rockspec and source rock for a new Luacheck release given version number. # Should be executed from root Luacheck directory. # Resulting rockspec and rock will be in `package/`. version="$1" rm -rf package mkdir package cd package echo echo "=== Creating rockspec for Luacheck $version ===" echo luarocks new-version ../luacheck-scm-1.rockspec --tag="$version" echo echo "=== Copying Luacheck files ===" echo mkdir luacheck cp -r ../src luacheck mkdir luacheck/bin cp ../bin/luacheck.lua luacheck/bin cp -r ../doc luacheck cp ../README.md ../CHANGELOG.md ../LICENSE luacheck echo echo "=== Packing source rock for Luacheck $version ===" echo zip -r luacheck-"$version"-1.src.rock luacheck luacheck-"$version"-1.rockspec cd .. luacheck-0.21.1/spec/000077500000000000000000000000001315524664100143135ustar00rootroot00000000000000luacheck-0.21.1/spec/analyze_spec.lua000066400000000000000000000070561315524664100175030ustar00rootroot00000000000000local analyze = require "luacheck.analyze" local linearize = require "luacheck.linearize" local parser = require "luacheck.parser" local utils = require "luacheck.utils" local ChState = utils.class() function ChState.__init() end function ChState.warn_redefined() end function ChState.warn_global() end function ChState.warn_unused_label() end function ChState.warn_unused_variable() end function ChState.warn_unused_value() end function ChState.warn_unset() end local function get_line_(src) local ast = parser.parse(src) local chstate = ChState() return linearize(chstate, ast) end local function get_line(src) local ok, res = pcall(get_line_, src) if ok then return res elseif type(res) == "table" then return nil else error(res, 0) end end local function used_variables_to_string(item) local buf = {} for var, values in pairs(item.used_values) do local values_buf = {} for _, value in ipairs(values) do table.insert(values_buf, tostring(value.location.line) .. ":" .. tostring(value.location.column)) end table.insert(buf, var.name .. " = (" .. table.concat(values_buf, ", ") .. ")") end table.sort(buf) return item.tag .. ": " .. table.concat(buf, "; ") end local function get_used_variables_as_string(src) local line = get_line(src) analyze(ChState(), line) local buf = {} for _, item in ipairs(line.items) do if item.accesses and next(item.accesses) then assert.is_table(item.used_values) table.insert(buf, used_variables_to_string(item)) end end return table.concat(buf, "\n") end describe("analyze", function() describe("when resolving values", function() it("resolves values in linear cases", function() assert.equal([[ Eval: a = (1:7)]], get_used_variables_as_string([[ local a = 6 print(a) ]])) end) it("resolves values after ifs", function() assert.equal([[ Eval: a = (1:7, 4:4)]], get_used_variables_as_string([[ local a if expr then a = 5 end print(a) ]])) assert.equal([[ Eval: a = (4:4, 7:4, 10:7, 13:4)]], get_used_variables_as_string([[ local a = 3 if expr then a = 4 elseif expr then a = 5 a = 8 if expr then a = 7 end else a = 6 end print(a) ]])) end) it("resolves values after loops", function() assert.equal([[ Eval: a = (1:7, 5:7) Eval: a = (1:7, 5:7)]], get_used_variables_as_string([[ local a while not a do if expr then a = expr2 end end print(a) ]])) assert.equal([[ Set: k = (2:5) Eval: v = (2:8) Eval: a = (3:4); b = (1:10) Eval: a = (1:7, 3:4); b = (1:10)]], get_used_variables_as_string([[ local a, b = 1, 2 for k, v in pairs(t) do a = k if v then print(a, b) end end print(a, b) ]])) end) end) describe("when resolving upvalues", function() it("resolves set upvalues naively", function() assert.equal([[ Eval: f = (3:16) Eval: a = (1:7, 4:4)]], get_used_variables_as_string([[ local a local function f() a = 5 end f() print(a) ]])) end) it("naively determines where closure is live", function() assert.equal([[ Eval: a = (1:7) Eval: a = (1:7, 6:4)]], get_used_variables_as_string([[ local a = 4 print(a) local function f() a = 5 end print(a) ]])) end) it("naively determines where closure is live in loops", function() assert.equal([[ Eval: a = (1:7, 6:22) Eval: a = (1:7, 6:22)]], get_used_variables_as_string([[ local a = 4 repeat print(a) escape(function() a = 5 end) until a ]])) end) end) end) luacheck-0.21.1/spec/cache_spec.lua000066400000000000000000000243051315524664100170770ustar00rootroot00000000000000local cache = require "luacheck.cache" local utils = require "luacheck.utils" local actual_format_version setup(function() actual_format_version = cache.format_version cache.format_version = 0 end) teardown(function() cache.format_version = actual_format_version end) describe("cache", function() describe("serialize", function() -- luacheck: no max line length it("returns serialized result", function() assert.same( [[return {{{"111","foo",5,100,102,[23]={"faa"}},{"211","bar",4,1,3,[8]=true},{"011",[4]=100000,[12]="near '\"'"}},{}}]], cache.serialize({ events = { {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 = {} }) ) 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,[8]=true},{"011",[4]=100000,[12]="near '\"'"}},{},{}}]], cache.serialize({ events = { {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 = {}, 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"}},{},{}}', 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 = {}, line_lengths = {} }) ) end) it("handles error result", function() assert.same('return {{{"011",[3]=2,[4]=4,[12]="message"}},{},{}}', cache.serialize({ events = { {code = "011", line = 2, column = 4, msg = "message"} }, per_line_options = {}, line_lengths = {} })) end) end) describe("update", function() local tmpname before_each(function() tmpname = os.tmpname() -- Work around os.tmpname being broken on Windows sometimes. if utils.is_windows and not tmpname:find(':') then tmpname = os.getenv("TEMP") .. tmpname end end) after_each(function() os.remove(tmpname) end) local function report(code) return { events = { code and {code = code} }, per_line_options = {}, line_lengths = {} } end it("creates new cache", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {report "111", report(), report "112"}) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 1 return {{{"112"}},{},{}} bar 2 return {{},{},{}} ]], data) end) it("appends new entries", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {report "111", report(), report "112"}) local ok, appended = cache.update(tmpname, {"baz"}, {3}, {report "122"}) assert.is_true(ok) assert.is_true(appended) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 1 return {{{"112"}},{},{}} bar 2 return {{},{},{}} baz 3 return {{{"122"}},{},{}} ]], data) end) it("overwrites old entries", function() cache.update(tmpname, {"foo", "bar", "foo"}, {1, 2, 1}, {report "111", report(), report "112"}) local ok, appended = cache.update(tmpname, {"baz", "foo"}, {3, 4}, {report "122", report()}) assert.is_true(ok) assert.is_false(appended) local data = utils.read_file(tmpname) assert.equals([[ 0 foo 4 return {{},{},{}} bar 2 return {{},{},{}} baz 3 return {{{"122"}},{},{}} ]], data) end) end) describe("load", function() describe("error handling", function() it("returns {} on cache with bad version", function() assert.same({}, cache.load("spec/caches/different_format.cache", {"foo"}, {123})) end) it("returns {} on cache without version", function() assert.same({}, cache.load("spec/caches/old_format.cache", {"foo"}, {123})) end) it("returns nil on cache with bad number of lines", function() assert.is_nil(cache.load("spec/caches/bad_lines.cache", {"foo"}, {123})) end) it("returns nil on cache with bad mtime", function() assert.is_nil(cache.load("spec/caches/bad_mtime.cache", {"foo"}, {123})) end) it("returns nil on cache with bad result", function() assert.is_nil(cache.load("spec/caches/bad_result.cache", {"foo"}, {123})) assert.is_nil(cache.load("spec/caches/bad_result2.cache", {"foo"}, {123})) end) end) describe("loading", function() local tmpname local foo_report = { events = { {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} } }, line_lengths = {10, 20, 30} } local bar_report = { events = {{code = "011", line = 2, column = 4, msg = "message"}}, per_line_options = {}, line_lengths = {40, 50} } before_each(function() tmpname = os.tmpname() cache.update(tmpname, {"foo", "bar"}, {1, 2}, {foo_report, bar_report}) end) after_each(function() os.remove(tmpname) end) it("loads {} from non-existent cache", function() assert.same({}, cache.load("non-existent.file", {"foo"})) end) it("loads cached results", function() assert.same({ foo = foo_report, bar = bar_report }, cache.load(tmpname, {"foo", "bar"}, {1, 2})) end) it("does not load results for missing files", function() assert.same({foo = foo_report}, cache.load(tmpname, {"foo", "baz"}, {1, 2})) end) it("does not load outdated results", function() assert.same( {bar = bar_report}, cache.load(tmpname, {"foo", "bar", "baz"}, {2, 2})) end) end) end) end) luacheck-0.21.1/spec/caches/000077500000000000000000000000001315524664100155415ustar00rootroot00000000000000luacheck-0.21.1/spec/caches/bad_lines.cache000066400000000000000000000000131315524664100204400ustar00rootroot00000000000000 0 foo 123 luacheck-0.21.1/spec/caches/bad_mtime.cache000066400000000000000000000000251315524664100204440ustar00rootroot00000000000000 0 foo bar return {} luacheck-0.21.1/spec/caches/bad_result.cache000066400000000000000000000000241315524664100206460ustar00rootroot00000000000000 0 foo 123 return { luacheck-0.21.1/spec/caches/bad_result2.cache000066400000000000000000000000321315524664100207270ustar00rootroot00000000000000 0 foo 123 return (nil)() luacheck-0.21.1/spec/caches/different_format.cache000066400000000000000000000000261315524664100220420ustar00rootroot00000000000000 -1 foo 123 return {} luacheck-0.21.1/spec/caches/old_format.cache000066400000000000000000000000221315524664100206460ustar00rootroot00000000000000foo 123 return {} luacheck-0.21.1/spec/check_spec.lua000066400000000000000000000616431315524664100171170ustar00rootroot00000000000000local check_full = require "luacheck.check" local function check(src) return check_full(src).events end describe("check", function() it("does not find anything wrong in an empty block", function() assert.same({}, check("")) end) it("does not find anything wrong in used locals", function() assert.same({ {code = "113", name = "print", indexing = {"print"}, line = 5, column = 4, end_column = 8} }, check[[ local a local b = 5 a = 6 do print(b, {a}) end ]]) end) it("detects global set", function() assert.same({ {code = "111", name = "foo", indexing = {"foo"}, line = 1, column = 1, end_column = 3, top = true} }, check[[ foo = {} ]]) end) it("detects global set in nested functions", function() assert.same({ {code = "111", name = "foo", indexing = {"foo"}, line = 2, column = 4, end_column = 6} }, check[[ local function bar() foo = {} end bar() ]]) end) it("detects global access in multi-assignments", function() assert.same({ {code = "111", name = "y", indexing = {"y"}, line = 2, column = 4, end_column = 4, top = true}, {code = "532", line = 2, column = 6, end_column = 6}, {code = "113", name = "print", indexing = {"print"}, line = 3, column = 1, end_column = 5} }, check[[ local x x, y = 1 print(x) ]]) end) it("detects global access in self swap", function() assert.same({ {code = "113", name = "a", indexing = {"a"}, line = 1, column = 11, end_column = 11}, {code = "113", name = "print", indexing = {"print"}, line = 2, column = 1, end_column = 5} }, check[[ local a = a print(a) ]]) end) it("detects global mutation", function() assert.same({ {code = "112", name = "a", indexing = {"a", false}, line = 1, column = 1, end_column = 1} }, check[[ a[1] = 6 ]]) end) it("detects indirect global field access", function() assert.same({ { code = "113", name = "b", indexing = {"b", false}, line = 2, column = 15, end_column = 15 }, { code = "113", name = "b", indexing = {"b", false, false, "foo"}, previous_indexing_len = 2, line = 3, column = 8, end_column = 12, indirect = true } }, check[[ local c = "foo" local alias = b[1] return alias[2][c] ]]) end) it("detects indirect global field mutation", function() assert.same({ { code = "113", name = "b", indexing = {"b", false}, line = 2, column = 15, end_column = 15 }, { code = "112", name = "b", indexing = {"b", false, false, "foo"}, previous_indexing_len = 2, line = 3, column = 1, end_column = 5, indirect = true } }, check[[ local c = "foo" local alias = b[1] alias[2][c] = c ]]) end) it("provides indexing information for warnings related to globals", function() assert.same({ { code = "113", name = "global", indexing = {"global"}, line = 2, column = 11, end_column = 16 }, { code = "113", name = "global", indexing = {"global", "foo", "bar", false}, indirect = true, previous_indexing_len = 1, line = 3, column = 15, end_column = 15 }, { code = "113", name = "global", indexing = {"global", "foo", "bar", false, true}, indirect = true, previous_indexing_len = 4, line = 5, column = 8, end_column = 13 } }, check[[ local c = "foo" local g = global local alias = g[c].bar[1] local alias2 = alias return alias2[...] ]]) end) it("detects unused locals", function() assert.same({ {code = "211", name = "a", line = 1, column = 7, end_column = 7}, {code = "113", name = "print", indexing = {"print"}, line = 5, column = 4, end_column = 8} }, check[[ local a = 4 do local b = 6 print(b) end ]]) end) it("detects useless local _ variable", function() assert.same({ {code = "211", name = "_", useless = true, line = 2, column = 10, end_column = 10}, {code = "211", name = "_", useless = true, line = 7, column = 13, end_column = 13}, {code = "211", name = "_", secondary = true, line = 12, column = 13, end_column = 13} }, check[[ do local _ end do local a = 5 local b, _ = a b() end do local c, _ = ... c() end ]]) end) it("reports unused function with forward declaration as variable, not value", function() assert.same({ {code = "211", name = "noop", func = true, line = 1, column = 22, end_column = 25} }, check[[ local noop; function noop() end ]]) end) it("detects unused recursive functions", function() assert.same({ {code = "211", name = "f", func = true, recursive = true, line = 1, column = 16, end_column = 16} }, check[[ local function f(x) return x <= 1 and 1 or x * f(x - 1) end ]]) end) it("detects unused mutually recursive functions", function() assert.same({ {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} }, check[[ 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("does not incorrectly detect unused recursive functions inside unused functions", function() assert.same({ {code = "211", name = "unused", func = true, line = 1, column = 16, end_column = 21} }, check[[ 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.same({ {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} }, check[[ local function f() return 1 end local function g() return f() + g() end ]]) assert.same({ {code = "211", name = "g", func = true, recursive = true, line = 2, column = 16, end_column = 16} }, check[[ local f local function g() return f() + g() end function f() return 1 end ]]) end) it("detects unused locals from function arguments", function() assert.same({ {code = "212", name = "foo", line = 1, column = 17, end_column = 19} }, check[[ return function(foo, ...) return ... end ]]) end) it("detects unused implicit self", function() assert.same({ {code = "212", name = "self", self = true, line = 2, column = 11, end_column = 11} }, check[[ local a = {} function a:b() end return a ]]) end) it("detects unused locals from loops", function() assert.same({ {code = "213", name = "i", line = 1, column = 5, end_column = 5}, {code = "213", name = "i", line = 2, column = 5, end_column = 5}, {code = "113", name = "pairs", indexing = {"pairs"}, line = 2, column = 10, end_column = 14} }, check[[ for i=1, 2 do end for i in pairs{} do end ]]) end) it("detects unused values", function() assert.same({ {code = "311", name = "a", line = 3, column = 4, end_column = 4, overwritten_line = 3, overwritten_column = 7}, {code = "311", name = "a", line = 3, column = 7, end_column = 7, overwritten_line = 8, overwritten_column = 1}, {code = "311", name = "a", line = 5, column = 4, end_column = 4, overwritten_line = 8, overwritten_column = 1} }, check[[ local a if ... then a, a = 2, 4 else a = 3 end a = 5 return a ]]) end) it("does not provide overwriting location if value can reach end of scope", function() assert.same({ {code = "311", name = "a", line = 4, column = 4, end_column = 4}, {code = "311", name = "a", line = 7, column = 7, end_column = 7} }, check[[ do local a = 1 (...)(a) a = 2 if ... then a = 3 end end ]]) end) it("does not detect unused value when it and a closure using it can live together", function() assert.same({ {code = "113", name = "escape", indexing = {"escape"}, line = 3, column = 4, end_column = 9} }, check[[ local a = 3 if true then escape(function() return a end) end ]]) end) it("does not consider value assigned to upvalue as unused if it is accessed in another closure", function() assert.same({}, check[[ local a local function f(x) a = x end local function g() return a end return f, g ]]) end) it("does not consider a variable initialized if it can't get a value due to short rhs", function() assert.same({}, check[[ local a, b = "foo" b = "bar" return a, b ]]) end) it("considers a variable initialized if short rhs ends with potential multivalue", function() assert.same({ {code = "311", name = "b", line = 2, column = 13, end_column = 13, secondary = true, overwritten_line = 3, overwritten_column = 4} }, check[[ return function(...) local a, b = ... b = "bar" return a, b end ]]) end) it("reports unused variable as secondary if it is assigned together with a used one", function() assert.same({ {code = "211", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f) local a, b = f() return b end ]]) end) it("reports unused value as secondary if it is assigned together with a used one", function() assert.same({ {code = "231", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f) local a, b a, b = f() return b end ]]) assert.same({ {code = "231", name = "a", line = 2, column = 10, end_column = 10, secondary = true} }, check[[ return function(f, t) local a a, t[1] = f() end ]]) end) it("detects variable that is mutated but never accessed", function() assert.same({ {code = "241", name = "a", line = 1, column = 7, end_column = 7} }, check[[ local a = {} a.k = 1 ]]) assert.same({ {code = "241", name = "a", line = 1, column = 7, end_column = 7} }, check[[ local a if ... then a = {} a.k1 = 1 else a = {} a.k2 = 2 end ]]) assert.same({ {code = "241", name = "a", line = 1, column = 7, end_column = 7}, {code = "311", name = "a", line = 7, column = 4, end_column = 4} }, check[[ local a if ... then a = {} a.k1 = 1 else a = {} end ]]) end) it("detects values that are mutated but never accessed", function() assert.same({ {code = "331", name = "a", line = 5, column = 4, end_column = 4} }, check[[ local a local b = (...).k if (...)[1] then a = {} a.k1 = 1 elseif (...)[2] then a = b a.k2 = 2 elseif (...)[3] then a = b() a.k3 = 3 elseif (...)[4] then a = b(1) or b(2) a.k4 = 4 else a = {} return a end ]]) 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}, {code = "314", field = "2", index = true, line = 6, column = 4, end_column = 4, overwritten_line = 9, overwritten_column = 4}, {code = "314", field = "key", line = 7, column = 4, end_column = 6, overwritten_line = 8, overwritten_column = 4}, {code = "314", field = "0.2e1", line = 9, column = 4, end_column = 4, overwritten_line = 10, overwritten_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}, {code = "311", name = "b", line = 1, column = 10, end_column = 10, overwritten_line = 2, overwritten_column = 4}, {code = "532", line = 2, column = 6, end_column = 6} }, check[[ local a, b = "foo", "bar" a, b = "bar" return a, b ]]) end) it("reports vartype == var when the unused value is not the initial", function() assert.same({ {code = "312", name = "b", line = 1, column = 23, end_column = 23, overwritten_line = 4, overwritten_column = 4}, {code = "311", name = "a", line = 2, column = 4, end_column = 4, overwritten_line = 3, overwritten_column = 4} }, check[[ local function foo(a, b) a = a or "default" a = 42 b = 7 return a, b end return foo ]]) end) it("does not detect unused values in loops", function() assert.same({ {code = "113", name = "print", indexing = {"print"}, line = 3, column = 4, end_column = 8}, {code = "113", name = "math", indexing = {"math", "floor"}, line = 4, column = 8, end_column = 11} }, check[[ local a = 10 while a > 0 do print(a) a = math.floor(a/2) end ]]) end) it("detects unused local value referred to from closure in incompatible branch", function() assert.same({ {code = "311", name = "a", line = 4, column = 4, end_column = 4}, {code = "321", name = "a", line = 6, column = 28, end_column = 28} }, check[[ local a if (...)() then a = 1 else (...)(function() return a end) end ]]) end) it("detects unused upvalue value referred to from closure in incompatible branch", function() assert.same({ {code = "311", name = "a", line = 4, column = 21, end_column = 21}, {code = "321", name = "a", line = 6, column = 28, end_column = 28} }, check[[ local a if (...)() then (...)(function() a = 1 end) else (...)(function() return a end) end ]]) end) it("handles upvalues before infinite loops", function() assert.same({ {code = "221", name = "x", line = 1, column = 7, end_column = 7}, {code = "211", name = "f", func = true, line = 2, column = 16, end_column = 16} }, check[[ local x local function f() return x end ::loop:: goto loop ]]) end) it("detects redefinition in the same scope", function() assert.same({ {code = "211", name = "foo", line = 1, column = 7, end_column = 9}, {code = "411", name = "foo", line = 2, column = 7, end_column = 9, prev_line = 1, prev_column = 7}, {code = "113", name = "print", indexing = {"print"}, line = 3, column = 1, end_column = 5} }, check[[ local foo local foo = "bar" print(foo) ]]) end) it("detects redefinition of function arguments", function() assert.same({ {code = "212", name = "foo", line = 1, column = 17, end_column = 19}, {code = "212", name = "...", line = 1, column = 22, end_column = 24}, {code = "412", name = "foo", line = 2, column = 10, end_column = 12, prev_line = 1, prev_column = 17} }, check[[ return function(foo, ...) local foo = 1 return foo end ]]) end) it("marks redefinition of implicit self", function() assert.same({ {code = "212", name = "self", line = 2, column = 11, end_column = 11, self = true}, {code = "212", name = "self", line = 4, column = 14, end_column = 14, self = true}, {code = "432", name = "self", line = 4, column = 14, end_column = 14, self = true, prev_line = 2, prev_column = 11} }, check[[ local t = {} function t:f() local o = {} function o:g() end return o end return t ]]) assert.same({ {code = "212", name = "self", line = 2, column = 14, end_column = 17}, {code = "212", name = "self", line = 4, column = 14, end_column = 14, self = true}, {code = "432", name = "self", line = 4, column = 14, end_column = 14, prev_line = 2, prev_column = 14} }, check[[ local t = {} function t.f(self) local o = {} function o:g() end return o end return t ]]) assert.same({ {code = "212", name = "self", line = 2, column = 11, end_column = 11, self = true}, {code = "212", name = "self", line = 4, column = 17, end_column = 20}, {code = "432", name = "self", line = 4, column = 17, end_column = 20, prev_line = 2, prev_column = 11} }, check[[ local t = {} function t:f() local o = {} function o.g(self) end return o end return t ]]) end) it("detects shadowing definitions", function() assert.same({ {code = "431", name = "a", line = 4, column = 10, end_column = 10, prev_line = 1, prev_column = 7}, {code = "421", name = "a", line = 7, column = 13, end_column = 13, prev_line = 4, prev_column = 10} }, check[[ local a = 46 return a, function(foo, ...) local a = 1 do local a = 6 foo(a, ...) end return a end ]]) end) it("detects unset variables", function() assert.same({ {code = "221", name = "a", line = 1, column = 7, end_column = 7} }, check[[ local a return a ]]) end) it("detects unused labels", function() assert.same({ {code = "521", label = "fail", line = 2, column = 4, end_column = 11} }, check[[ ::fail:: do ::fail:: end goto fail ]]) end) it("detects unreachable code", function() assert.same({ {code = "511", line = 2, column = 1, end_column = 2} }, check[[ do return end if ... then return 6 end return 3 ]]) assert.same({ {code = "511", line = 7, column = 1, end_column = 2}, {code = "511", line = 13, column = 1, end_column = 6} }, check[[ if ... then return 4 else return 6 end if ... then return 7 else return 8 end return 3 ]]) end) it("detects unreachable code with literal conditions", function() assert.same({ {code = "511", line = 4, column = 1, end_column = 6} }, check[[ while true do (...)() end return ]]) assert.same({}, check[[ repeat if ... then break end until false return ]]) assert.same({ {code = "511", line = 6, column = 1, end_column = 6} }, check[[ repeat if nil then break end until false return ]]) end) it("detects unreachable expressions", function() assert.same({ {code = "511", line = 3, column = 7, end_column = 9} }, check[[ repeat return until ... ]]) assert.same({ {code = "511", line = 3, column = 8, end_column = 10} }, check[[ if true then (...)() elseif ... then (...)() end ]]) end) it("detects unreachable functions", function() assert.same({ {code = "231", name = "f", line = 1, column = 7, end_column = 7}, {code = "511", line = 3, column = 1, end_column = 8} }, check[[ local f = nil do return end function f() end ]]) end) it("detects unreachable code in nested function", function() assert.same({ {code = "511", line = 4, column = 7, end_column = 12} }, check[[ return function() return function() do return end return end end ]]) end) it("detects accessing uninitialized variables", function() assert.same({ {code = "113", name = "get", indexing = {"get"}, line = 6, column = 8, end_column = 10}, {code = "321", name = "a", line = 6, column = 12, end_column = 12} }, check[[ local a if ... then a = 5 else a = get(a) end return a ]]) end) it("detects mutating uninitialized variables", function() assert.same({ {code = "341", name = "a", line = 4, column = 4, end_column = 4}, {code = "113", name = "get", indexing = {"get"}, line = 6, column = 8, end_column = 10} }, check[[ 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.same({ {code = "113", name = "get", indexing = {"get"}, line = 7, column = 8, end_column = 10}, {code = "321", name = "a", line = 7, column = 12, end_column = 12} }, check[[ return function() return function(...) local a if ... then a = 5 else a = get(a) end return a end end ]]) end) it("does not detect accessing unitialized variables incorrectly in loops", function() assert.same({ {code = "113", name = "get", indexing = {"get"}, line = 4, column = 8, end_column = 10} }, check[[ local a while not a do a = get() end return a ]]) end) it("detects unbalanced assignments", function() assert.same({ {code = "532", line = 4, column = 6, end_column = 6}, {code = "531", line = 5, column = 6, end_column = 6} }, check[[ local a, b = 4; (...)(a) a, b = (...)(); (...)(a, b) a, b = 5; (...)(a, b) a, b = 1, 2, 3; (...)(a, b) ]]) end) it("detects empty blocks", function() assert.same({ {code = "541", line = 1, column = 1, end_column = 2}, {code = "542", line = 3, column = 8, end_column = 11}, {code = "542", line = 5, column = 12, end_column = 15}, {code = "542", line = 7, column = 1, end_column = 4} }, check[[ do end if ... then elseif ... then else end while ... do end repeat until ... ]]) end) it("detects empty statements", function() assert.same({ {code = "551", line = 1, column = 1, end_column = 1}, {code = "541", line = 2, column = 1, end_column = 2}, {code = "551", line = 2, column = 8, end_column = 8}, {code = "551", line = 4, column = 20, end_column = 20}, {code = "551", line = 7, column = 17, end_column = 17} }, check[[ ; do end;; local foo = "bar"; foo = foo .. "baz";; while true do if foo() then; goto fail; elseif foo() then break; end end ::fail:: return foo; ]]) end) it("emits events, per-line options, and line lengths", 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 = "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 = "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} }, per_line_options = { [2] = {{options = {ignore = {"foo"}}, line = 2, column = 16, end_column = 38}} }, line_lengths = {28, 38, 16, 17, 19, 17, 32, 16, 3}, line_endings = {"comment", "comment", "comment", nil, "comment", "comment", nil, "comment", nil} }, check_full[[ -- luacheck: push ignore bar local foo, bar -- luacheck: ignore foo -- luacheck: pop return function() -- luacheck: ignore -- luacheck: push for _ in pairs({}) do return end -- luacheck: pop end ]]) end) it("emits correct inline option error messages", function() assert.same({ {code = "023", line = 1, column = 1, end_column = 16}, {code = "022", line = 2, column = 1, end_column = 17}, {code = "021", msg = "unknown inline option 'something strange'", line = 3, column = 1, end_column = 30}, {code = "021", msg = "inline option 'std' expects 1 argument, 0 given", line = 4, column = 1, end_column = 16}, {code = "021", msg = "inline option 'std' expects 1 argument, 3 given", line = 5, column = 1, end_column = 30}, {code = "021", msg = "inline option 'no unused' expects 0 arguments, 2 given", line = 6, column = 1, end_column = 43}, {code = "021", msg = "unknown inline option 'no ignore anything please'", line = 7, column = 1, end_column = 38}, {code = "021", msg = "empty inline option", line = 8, column = 1, end_column = 12}, {code = "021", msg = "empty inline option invocation", line = 9, column = 1, end_column = 38} }, check_full[[ -- luacheck: pop -- luacheck: push -- luacheck: something strange -- luacheck: std -- luacheck: std lua51 + lua52 -- luacheck: no unused, no unused very much -- luacheck: no ignore anything please -- luacheck: -- luacheck: no unused, , no redefined ]].events) end) it("handles argparse sample", function() assert.table(check(io.open("spec/samples/argparse.lua", "rb"):read("*a"))) end) end) luacheck-0.21.1/spec/cli_spec.lua000066400000000000000000001774301315524664100166130ustar00rootroot00000000000000local utils = require "luacheck.utils" local multithreading = require "luacheck.multithreading" local helper = require "spec.helper" local luacheck_cmd = helper.luacheck_command() local function quote(argument) -- Do not worry about special characters too much, just quote. local mark = utils.is_windows and '"' or "'" return mark .. argument .. mark end local function norm_output(output) -- Replace "/" with native slashes, except when it's used to separate -- warning and error numbers on the last line. return output:gsub("(%w)/(%w)", "%1" .. utils.dir_sep .. "%2") end local function get_output(command, wd, color) if color then if utils.is_windows and not os.getenv("ANSICON") then pending("uses terminal colors") end else command = "--no-color " .. command end command = ("%s %s 2>&1"):format(helper.luacheck_command(wd), command) local handler = io.popen(command) local output = handler:read("*a") handler:close() if color then return (output:gsub("\27%[%d+m", "\27"):gsub("\27+", "#")) else return output end end local function get_exitcode(command) local nosql_db = package.config:sub(1, 1) == "/" and "/dev/null" or "NUL" local code51, _, code52plus = os.execute(luacheck_cmd.." "..command.." > "..nosql_db.." 2>&1") if type(code51) == "number" then return code51 >= 256 and math.floor(code51 / 256) or code51 else return code52plus end end -- luacheck: max line length 180 describe("cli", function() it("exists", function() assert.equal(0, get_exitcode "--help") end) it("handles invalid options", function() assert.equal(4, get_exitcode "--invalid-option") end) it("works for correct files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua --no-config") assert.equal(0, get_exitcode "spec/samples/good_code.lua --no-config") end) it("removes ./ in the beginnings of file names", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "./spec/samples/good_code.lua --no-config") end) it("allows setting new filename", function() assert.equal([[ Checking new.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/good_code.lua --no-config --filename new.lua") end) it("filters files using --exclude-files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-config --exclude-files " .. quote("**/??d_code.lua"))) end) it("filters files using --include-files", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings Total: 5 warnings / 0 errors in 1 file ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-config -qq --include-files " .. quote("**/??d_code.lua"))) end) it("--exclude-files has priority over --include-files", function() assert.equal([[ Checking spec/samples/good_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-config --include-files " .. quote("**/*.lua") .. " --exclude-files " .. quote("**/??d_code.lua"))) end) it("works for incorrect files", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --no-config") assert.equal(1, get_exitcode "spec/samples/bad_code.lua --no-config") end) it("detects whitespace issues", function() assert.equal([[ Checking spec/samples/bad_whitespace.lua 10 warnings spec/samples/bad_whitespace.lua:4:26: line contains trailing whitespace spec/samples/bad_whitespace.lua:8:25: trailing whitespace in a comment spec/samples/bad_whitespace.lua:14:20: trailing whitespace in a string spec/samples/bad_whitespace.lua:17:30: trailing whitespace in a comment spec/samples/bad_whitespace.lua:22:40: trailing whitespace in a comment spec/samples/bad_whitespace.lua:26:1: line contains only whitespace spec/samples/bad_whitespace.lua:27:1: line contains only whitespace spec/samples/bad_whitespace.lua:28:1: line contains only whitespace spec/samples/bad_whitespace.lua:29:1: line contains only whitespace spec/samples/bad_whitespace.lua:34:1: inconsistent indentation (SPACE followed by TAB) Total: 10 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_whitespace.lua --no-config") assert.equal(1, get_exitcode "spec/samples/bad_whitespace.lua --no-config") end) it("works for incorrect patterns in options", function() assert.equal([[ Critical error: Invalid pattern '^%1foo$' ]], get_output "spec/samples/bad_code.lua --ignore %1foo --no-config") end) it("checks stdin when given -", function() assert.equal([[ Checking stdin 5 warnings stdin:3:16: unused function 'helper' stdin:3:23: unused variable length argument stdin:7:10: setting non-standard global variable 'embrace' stdin:8:10: variable 'opt' was previously defined as an argument on line 7 stdin:9:11: accessing undefined variable 'hepler' Total: 5 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/override_config.luacheckrc < spec/samples/bad_code.lua") end) it("colors output by default", function() assert.equal([[ Checking spec/samples/good_code.lua #OK# Checking spec/samples/bad_code.lua #5 warnings# spec/samples/bad_code.lua:3:16: unused function #helper# spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable #embrace# spec/samples/bad_code.lua:8:10: variable #opt# was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable #hepler# Total: #5# warnings / #0# errors in 2 files ]], get_output("spec/samples/good_code.lua spec/samples/bad_code.lua --no-config", nil, true)) end) it("suppresses OK output with -q", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument 'baz' spec/samples/unused_code.lua:4:8: unused loop variable 'i' spec/samples/unused_code.lua:5:13: unused variable 'q' spec/samples/unused_code.lua:7:11: unused loop variable 'a' spec/samples/unused_code.lua:7:14: unused loop variable 'b' spec/samples/unused_code.lua:7:17: unused loop variable 'c' spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use spec/samples/unused_code.lua:21:7: variable 'z' is never accessed Total: 14 warnings / 0 errors in 3 files ]], get_output "-q spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config") assert.equal([[ Total: 0 warnings / 0 errors in 1 file ]], get_output "-q spec/samples/good_code.lua --no-config") end) it("suppresses warnings output with -qq", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings Checking spec/samples/unused_code.lua 9 warnings Total: 14 warnings / 0 errors in 3 files ]], get_output "-qq spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config") end) it("suppresses file info output with -qqq", function() assert.equal([[Total: 14 warnings / 0 errors in 3 files ]], get_output "-qqq spec/samples/bad_code.lua spec/samples/good_code.lua spec/samples/unused_code.lua --no-config") end) it("allows to ignore some types of warnings", function() assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 3 warnings / 0 errors in 1 file ]], get_output "-u spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 Total: 3 warnings / 0 errors in 1 file ]], get_output "-g spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 4 warnings / 0 errors in 1 file ]], get_output "-r spec/samples/bad_code.lua --no-config") end) it("allows to define additional globals", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --globals embrace --no-config") end) it("allows to set standard globals", function() assert.equal([[ Checking spec/samples/bad_code.lua 6 warnings spec/samples/bad_code.lua:1:1: mutating non-standard global variable 'package' spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 6 warnings / 0 errors in 1 file ]], get_output "--std none spec/samples/bad_code.lua --no-config") assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 5 warnings / 0 errors in 1 file ]], get_output "--std lua51+lua52+lua53 spec/samples/bad_code.lua --no-config") end) it("allows to ignore some variables", function() assert.equal([[ Checking spec/samples/bad_code.lua 3 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --ignore embrace opt --no-config") end) it("allows to only watch some variables", function() assert.equal([[ Checking spec/samples/bad_code.lua 1 warning spec/samples/bad_code.lua:3:16: unused function 'helper' Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/bad_code.lua --only helper --no-config") end) it("recognizes different types of variables", function() assert.equal([[ Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument 'baz' spec/samples/unused_code.lua:4:8: unused loop variable 'i' spec/samples/unused_code.lua:5:13: unused variable 'q' spec/samples/unused_code.lua:7:11: unused loop variable 'a' spec/samples/unused_code.lua:7:14: unused loop variable 'b' spec/samples/unused_code.lua:7:17: unused loop variable 'c' spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use spec/samples/unused_code.lua:21:7: variable 'z' is never accessed Total: 9 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_code.lua --no-config") end) it("allows to ignore unused arguments", function() assert.equal([[ Checking spec/samples/unused_code.lua 4 warnings spec/samples/unused_code.lua:5:13: unused variable 'q' spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use spec/samples/unused_code.lua:21:7: variable 'z' is never accessed Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_code.lua --no-unused-args --no-config") end) it("allows to ignore unused secondary values and variables", function() assert.equal([[ Checking spec/samples/unused_secondaries.lua 4 warnings spec/samples/unused_secondaries.lua:3:7: unused variable 'a' spec/samples/unused_secondaries.lua:6:7: unused variable 'x' spec/samples/unused_secondaries.lua:6:13: unused variable 'z' spec/samples/unused_secondaries.lua:12:1: value assigned to variable 'o' is unused Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/unused_secondaries.lua --no-config") assert.equal([[ Checking spec/samples/unused_secondaries.lua 1 warning spec/samples/unused_secondaries.lua:6:7: unused variable 'x' Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/unused_secondaries.lua -s --no-config") end) it("allows to ignore warnings related to implicit self", function() assert.equal([[ Checking spec/samples/redefined.lua 5 warnings spec/samples/redefined.lua:4:10: shadowing upvalue 'a' on line 1 spec/samples/redefined.lua:4:13: variable 'self' is never set spec/samples/redefined.lua:4:13: variable 'self' was previously defined as an argument on line 3 spec/samples/redefined.lua:7:13: shadowing definition of variable 'a' on line 4 spec/samples/redefined.lua:8:32: shadowing upvalue 'self' on line 4 Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/redefined.lua --no-self --globals each --no-config") end) it("handles errors gracefully", function() assert.equal([[ Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Checking s/samples/absent_code.lua I/O error s/samples/absent_code.lua: couldn't read: No such file or directory Total: 0 warnings / 1 error in 1 file, couldn't check 1 file ]], get_output "spec/samples/python_code.lua s/samples/absent_code.lua --no-config") assert.equal(3, get_exitcode "spec/samples/python_code.lua spec/samples/absent_code.lua --no-config") end) it("expands rockspecs", function() assert.equal([[ Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/good_code.lua OK Total: 5 warnings / 0 errors in 2 files ]], get_output "spec/samples/sample.rockspec --no-config") end) it("handles bad rockspecs", function() assert.equal([[ Checking spec/samples/bad.rockspec Syntax error spec/samples/bad.rockspec: rockspec.build is not a table Total: 0 warnings / 0 errors in 0 files, couldn't check 1 file ]], get_output "spec/samples/bad.rockspec --no-config") end) it("allows ignoring defined globals", function() assert.equal([[ Checking spec/samples/defined.lua 1 warning spec/samples/defined.lua:4:4: accessing undefined variable 'baz' Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined.lua spec/samples/defined2.lua -d --no-config") assert.equal([[ Checking spec/samples/defined2.lua OK Checking spec/samples/defined.lua 1 warning spec/samples/defined.lua:4:4: accessing undefined variable 'baz' Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined2.lua spec/samples/defined.lua -d --no-config") end) it("allows restricting scope of defined globals to the file with their definition", function() assert.equal([[ Checking spec/samples/defined2.lua 1 warning spec/samples/defined2.lua:1:1: accessing undefined variable 'foo' Checking spec/samples/defined3.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined2.lua spec/samples/defined3.lua -d -m --no-config") end) it("allows ignoring globals defined in top level scope", function() assert.equal([[ Checking spec/samples/defined4.lua 2 warnings spec/samples/defined4.lua:1:10: unused global variable 'foo' spec/samples/defined4.lua:3:4: setting non-standard global variable 'bar' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined4.lua -t --no-config") end) it("detects unused defined globals", function() assert.equal([[ Checking spec/samples/defined3.lua 3 warnings spec/samples/defined3.lua:1:1: unused global variable 'foo' spec/samples/defined3.lua:2:1: unused global variable 'foo' spec/samples/defined3.lua:3:1: unused global variable 'bar' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -d --no-config") assert.equal([[ Checking spec/samples/defined3.lua 1 warning spec/samples/defined3.lua:3:1: unused global variable 'bar' Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -d --no-config") end) it("treats `unused global` warnings as `global` type warnings", function() assert.equal([[ Checking spec/samples/defined3.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -gd --no-config") assert.equal([[ Checking spec/samples/defined3.lua 1 warning spec/samples/defined3.lua:3:1: unused global variable 'bar' Checking spec/samples/defined2.lua OK Total: 1 warning / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -ud --no-config") end) it("allows ignoring unused defined globals", function() assert.equal([[ Checking spec/samples/defined3.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/defined3.lua -d --ignore 13 --no-config") assert.equal([[ Checking spec/samples/defined3.lua OK Checking spec/samples/defined2.lua OK Total: 0 warnings / 0 errors in 2 files ]], get_output "spec/samples/defined3.lua spec/samples/defined2.lua -d --ignore 13 --no-config") end) it("detects flow issues", function() assert.equal([[ Checking spec/samples/bad_flow.lua 6 warnings spec/samples/bad_flow.lua:1:28: empty if branch spec/samples/bad_flow.lua:6:4: empty do..end block spec/samples/bad_flow.lua:12:15: 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:21:7: unreachable code spec/samples/bad_flow.lua:25:1: loop is executed at most once Total: 6 warnings / 0 errors in 1 file ]], get_output "spec/samples/bad_flow.lua --no-config") end) it("detects redefinitions", function() assert.equal([[ Checking spec/samples/redefined.lua 6 warnings spec/samples/redefined.lua:3:11: unused argument 'self' spec/samples/redefined.lua:4:10: shadowing upvalue 'a' on line 1 spec/samples/redefined.lua:4:13: variable 'self' is never set spec/samples/redefined.lua:4:13: variable 'self' was previously defined as an argument on line 3 spec/samples/redefined.lua:7:13: shadowing definition of variable 'a' on line 4 spec/samples/redefined.lua:8:32: shadowing upvalue 'self' on line 4 Total: 6 warnings / 0 errors in 1 file ]], get_output "spec/samples/redefined.lua --globals each --no-config") end) it("detects lines that are too long", function() assert.equal([[ Checking spec/samples/line_length.lua 8 warnings spec/samples/line_length.lua:2:121: line is too long (123 > 120) spec/samples/line_length.lua:3:121: line is too long (164 > 120) spec/samples/line_length.lua:8:121: line is too long (134 > 120) spec/samples/line_length.lua:13:41: line is too long (47 > 40) spec/samples/line_length.lua:18:121: line is too long (132 > 120) spec/samples/line_length.lua:22:81: line is too long (85 > 80) spec/samples/line_length.lua:26:101: line is too long (104 > 100) spec/samples/line_length.lua:29:121: line is too long (125 > 120) Total: 8 warnings / 0 errors in 1 file ]], get_output "spec/samples/line_length.lua --no-config") assert.equal([[ Checking spec/samples/line_length.lua 7 warnings spec/samples/line_length.lua:3:131: line is too long (164 > 130) spec/samples/line_length.lua:8:131: line is too long (134 > 130) spec/samples/line_length.lua:13:41: line is too long (47 > 40) spec/samples/line_length.lua:18:131: line is too long (132 > 130) spec/samples/line_length.lua:22:81: line is too long (85 > 80) spec/samples/line_length.lua:26:101: line is too long (104 > 100) spec/samples/line_length.lua:29:121: line is too long (125 > 120) Total: 7 warnings / 0 errors in 1 file ]], get_output "spec/samples/line_length.lua --no-config --max-line-length=130") assert.equal([[ Checking spec/samples/line_length.lua 4 warnings spec/samples/line_length.lua:13:41: line is too long (47 > 40) spec/samples/line_length.lua:22:81: line is too long (85 > 80) spec/samples/line_length.lua:26:101: line is too long (104 > 100) spec/samples/line_length.lua:29:121: line is too long (125 > 120) Total: 4 warnings / 0 errors in 1 file ]], get_output "spec/samples/line_length.lua --no-config --no-max-line-length") assert.equal([[ Checking spec/samples/line_length.lua 7 warnings spec/samples/line_length.lua:2:121: line is too long (123 > 120) spec/samples/line_length.lua:3:121: line is too long (164 > 120) spec/samples/line_length.lua:13:41: line is too long (47 > 40) spec/samples/line_length.lua:18:121: line is too long (132 > 120) spec/samples/line_length.lua:22:81: line is too long (85 > 80) spec/samples/line_length.lua:26:101: line is too long (104 > 100) spec/samples/line_length.lua:29:121: line is too long (125 > 120) Total: 7 warnings / 0 errors in 1 file ]], get_output "spec/samples/line_length.lua --no-config --no-max-string-line-length") end) it("detects issues related to read-only globals", function() assert.equal([[ Checking spec/samples/read_globals.lua 5 warnings spec/samples/read_globals.lua:1:1: setting read-only global variable 'string' spec/samples/read_globals.lua:2:1: setting undefined field 'append' of global 'table' spec/samples/read_globals.lua:5:1: setting read-only global variable 'bar' spec/samples/read_globals.lua:6:1: mutating non-standard global variable 'baz' spec/samples/read_globals.lua:6:21: accessing undefined variable 'baz' Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals.lua --std=lua52 --globals foo --read-globals bar --no-config") end) it("detects indirect global indexing", function() assert.equal([[ Checking spec/samples/indirect_globals.lua 3 warnings spec/samples/indirect_globals.lua:2:11-16: accessing undefined variable 'global' spec/samples/indirect_globals.lua:5:1-8: indirectly setting undefined field 'concat.foo.bar' of global 'table' spec/samples/indirect_globals.lua:5:32-37: accessing undefined variable 'global' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/indirect_globals.lua --std=min --ranges --no-config") end) it("allows defining fields", function() assert.equal([[ Checking spec/samples/indirect_globals.lua 2 warnings spec/samples/indirect_globals.lua:2:11-16: accessing undefined variable 'global' spec/samples/indirect_globals.lua:5:32-37: accessing undefined variable 'global' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/indirect_globals.lua --std=min --globals table.concat.foo --ranges --no-config") end) it("detects issues related to global fields", function() assert.equal([[ Checking spec/samples/global_fields.lua 13 warnings / 1 error spec/samples/global_fields.lua:2:16: indirectly accessing undefined field 'upsert' of global 'table' spec/samples/global_fields.lua:8:1: indirectly setting undefined field 'insert.foo' of global 'table' spec/samples/global_fields.lua:23:7: accessing undefined field 'gfind' of global 'string' spec/samples/global_fields.lua:27:7: accessing undefined field 'find' of global 'string' spec/samples/global_fields.lua:32:7: accessing undefined variable 'server' spec/samples/global_fields.lua:33:7: accessing undefined variable 'server' spec/samples/global_fields.lua:34:1: mutating non-standard global variable 'server' spec/samples/global_fields.lua:35:1: mutating non-standard global variable 'server' spec/samples/global_fields.lua:36:1: mutating non-standard global variable 'server' spec/samples/global_fields.lua:37:7: accessing undefined variable 'server' spec/samples/global_fields.lua:38:1: mutating non-standard global variable 'server' spec/samples/global_fields.lua:40:1: invalid value of inline option 'std' spec/samples/global_fields.lua:41:1: mutating non-standard global variable 'server' spec/samples/global_fields.lua:42:1: mutating non-standard global variable 'server' Total: 13 warnings / 1 error in 1 file ]], get_output "spec/samples/global_fields.lua --no-config") assert.equal([[ Checking spec/samples/global_fields.lua 7 warnings spec/samples/global_fields.lua:2:16: indirectly accessing undefined field 'upsert' of global 'table' spec/samples/global_fields.lua:8:1: indirectly setting undefined field 'insert.foo' of global 'table' spec/samples/global_fields.lua:23:7: accessing undefined field 'gfind' of global 'string' spec/samples/global_fields.lua:27:7: accessing undefined field 'find' of global 'string' spec/samples/global_fields.lua:34:1: setting undefined field 'foo' of global 'server' spec/samples/global_fields.lua:35:1: setting undefined field 'bar.?' of global 'server' spec/samples/global_fields.lua:37:7: accessing undefined field 'baz.abcd' of global 'server' Total: 7 warnings / 0 errors in 1 file ]], get_output "spec/samples/global_fields.lua --config=spec/configs/custom_fields_config.luacheckrc") end) it("allows showing warning codes", function() assert.equal([[ Checking spec/samples/read_globals.lua 5 warnings spec/samples/read_globals.lua:1:1: (W121) setting read-only global variable 'string' spec/samples/read_globals.lua:2:1: (W142) setting undefined field 'append' of global 'table' spec/samples/read_globals.lua:5:1: (W121) setting read-only global variable 'bar' spec/samples/read_globals.lua:6:1: (W112) mutating non-standard global variable 'baz' spec/samples/read_globals.lua:6:21: (W113) accessing undefined variable 'baz' Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals.lua --std=lua52 --globals foo --read-globals bar --codes --no-config") end) it("allows showing token ranges", function() assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors spec/samples/inline_options.lua:6:16-16: unused function 'f' spec/samples/inline_options.lua:12:4-5: accessing undefined variable 'qu' spec/samples/inline_options.lua:15:1-3: accessing undefined variable 'baz' spec/samples/inline_options.lua:22:10-10: unused variable 'g' spec/samples/inline_options.lua:24:7-7: unused variable 'f' spec/samples/inline_options.lua:24:10-10: unused variable 'g' spec/samples/inline_options.lua:26:1-17: unpaired push directive spec/samples/inline_options.lua:28:4-19: unpaired pop directive spec/samples/inline_options.lua:34:1-2: empty do..end block spec/samples/inline_options.lua:35:10-13: empty if branch Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6-15: expected '=' near '__future__' Total: 8 warnings / 3 errors in 2 files ]], get_output "spec/samples/inline_options.lua spec/samples/python_code.lua --ranges --no-config") end) it("applies inline options", function() assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors spec/samples/inline_options.lua:6:16: unused function 'f' spec/samples/inline_options.lua:12:4: accessing undefined variable 'qu' spec/samples/inline_options.lua:15:1: accessing undefined variable 'baz' spec/samples/inline_options.lua:22:10: unused variable 'g' spec/samples/inline_options.lua:24:7: unused variable 'f' spec/samples/inline_options.lua:24:10: unused variable 'g' spec/samples/inline_options.lua:26:1: unpaired push directive spec/samples/inline_options.lua:28:4: unpaired pop directive spec/samples/inline_options.lua:34:1: empty do..end block spec/samples/inline_options.lua:35:10: empty if branch Total: 8 warnings / 2 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --no-config") -- Inline `enable` option overrides CLI `ignore`. assert.equal([[ Checking spec/samples/inline_options.lua 8 warnings / 2 errors spec/samples/inline_options.lua:6:16: unused function 'f' spec/samples/inline_options.lua:12:4: accessing undefined variable 'qu' spec/samples/inline_options.lua:15:1: accessing undefined variable 'baz' spec/samples/inline_options.lua:22:10: unused variable 'g' spec/samples/inline_options.lua:24:7: unused variable 'f' spec/samples/inline_options.lua:24:10: unused variable 'g' spec/samples/inline_options.lua:26:1: unpaired push directive spec/samples/inline_options.lua:28:4: unpaired pop directive spec/samples/inline_options.lua:34:1: empty do..end block spec/samples/inline_options.lua:35:10: empty if branch Total: 8 warnings / 2 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --ignore=542 --no-config") assert.equal([[ Checking spec/samples/global_inline_options.lua 3 warnings spec/samples/global_inline_options.lua:6:10: unused global variable 'f' spec/samples/global_inline_options.lua:7:4: setting non-standard global variable 'baz' spec/samples/global_inline_options.lua:18:4: setting non-module global variable 'external' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/global_inline_options.lua --std=lua52 --no-config") assert.equal([[ Checking spec/samples/read_globals_inline_options.lua 5 warnings spec/samples/read_globals_inline_options.lua:2:10: accessing undefined variable 'baz' spec/samples/read_globals_inline_options.lua:3:1: setting read-only global variable 'foo' spec/samples/read_globals_inline_options.lua:3:11: setting non-standard global variable 'baz' spec/samples/read_globals_inline_options.lua:3:16: mutating non-standard global variable 'baz' spec/samples/read_globals_inline_options.lua:5:1: setting read-only global variable 'foo' Total: 5 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals_inline_options.lua --std=lua52 --no-config") assert.equal([[ Checking spec/samples/read_globals_inline_options.lua 3 warnings spec/samples/read_globals_inline_options.lua:3:1: setting read-only global variable 'foo' spec/samples/read_globals_inline_options.lua:3:16: setting read-only field '?' of global 'baz' spec/samples/read_globals_inline_options.lua:5:1: setting read-only global variable 'foo' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/read_globals_inline_options.lua --std=lua52 --read-globals baz --globals foo --no-config") end) it("inline options can use extended stds", function() assert.equal([[ Checking spec/samples/custom_std_inline_options.lua 3 warnings spec/samples/custom_std_inline_options.lua:3:1: accessing undefined variable 'tostring' spec/samples/custom_std_inline_options.lua:6:19: accessing undefined variable 'print' spec/samples/custom_std_inline_options.lua:6:25: accessing undefined variable 'it' Total: 3 warnings / 0 errors in 1 file ]], get_output "spec/samples/custom_std_inline_options.lua --config=spec/configs/custom_stds_config.luacheckrc") end) it("inline options can be disabled", function() assert.equal([[ Checking spec/samples/inline_options.lua 26 warnings spec/samples/inline_options.lua:3:1: accessing undefined variable 'foo' spec/samples/inline_options.lua:4:1: accessing undefined variable 'bar' spec/samples/inline_options.lua:6:16: unused function 'f' spec/samples/inline_options.lua:6:18: unused argument 'a' spec/samples/inline_options.lua:8:4: accessing undefined variable 'foo' spec/samples/inline_options.lua:9:4: accessing undefined variable 'bar' spec/samples/inline_options.lua:10:4: accessing undefined variable 'baz' spec/samples/inline_options.lua:11:4: accessing undefined variable 'qu' spec/samples/inline_options.lua:12:4: accessing undefined variable 'qu' spec/samples/inline_options.lua:15:1: accessing undefined variable 'baz' spec/samples/inline_options.lua:18:7: unused variable 'f' spec/samples/inline_options.lua:18:7: variable 'f' was previously defined on line 6 spec/samples/inline_options.lua:20:7: unused variable 'g' spec/samples/inline_options.lua:22:7: unused variable 'f' spec/samples/inline_options.lua:22:7: variable 'f' was previously defined on line 18 spec/samples/inline_options.lua:22:10: unused variable 'g' spec/samples/inline_options.lua:22:10: variable 'g' was previously defined on line 20 spec/samples/inline_options.lua:24:7: unused variable 'f' spec/samples/inline_options.lua:24:7: variable 'f' was previously defined on line 22 spec/samples/inline_options.lua:24:10: unused variable 'g' spec/samples/inline_options.lua:24:10: variable 'g' was previously defined on line 22 spec/samples/inline_options.lua:27:16: unused function 'f' spec/samples/inline_options.lua:27:16: variable 'f' was previously defined on line 24 spec/samples/inline_options.lua:32:1: empty do..end block spec/samples/inline_options.lua:34:1: empty do..end block spec/samples/inline_options.lua:35:10: empty if branch Total: 26 warnings / 0 errors in 1 file ]], get_output "spec/samples/inline_options.lua --std=none --no-inline --no-config") end) describe("caching", function() local tmpname before_each(function() tmpname = os.tmpname() -- Work around os.tmpname being broken on Windows sometimes. if utils.is_windows and not tmpname:find(':') then tmpname = os.getenv("TEMP") .. tmpname end end) after_each(function() os.remove(tmpname) end) it("caches results", function() local normal_output = [[ Checking spec/samples/good_code.lua OK Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Total: 5 warnings / 1 error in 3 files ]] local mocked_output = [[ Checking spec/samples/good_code.lua 1 error spec/samples/good_code.lua:5:7: this code is actually bad Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/python_code.lua 2 warnings spec/samples/python_code.lua:1:1: setting non-standard global variable 'global' spec/samples/python_code.lua:6:8: accessing uninitialized variable 'uninit' Checking spec/samples/unused_code.lua 9 warnings spec/samples/unused_code.lua:3:18: unused argument 'baz' spec/samples/unused_code.lua:4:8: unused loop variable 'i' spec/samples/unused_code.lua:5:13: unused variable 'q' spec/samples/unused_code.lua:7:11: unused loop variable 'a' spec/samples/unused_code.lua:7:14: unused loop variable 'b' spec/samples/unused_code.lua:7:17: unused loop variable 'c' spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use spec/samples/unused_code.lua:21:7: variable 'z' is never accessed Total: 16 warnings / 1 error in 4 files ]] assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) local cache = utils.read_file(tmpname) assert.string(cache) -- luacheck: push no max string line length local format_version, good_mtime, bad_mtime, python_mtime = cache:match((([[ (%d+) spec/samples/good_code.lua (%d+) return {{},{},{19,0,23,17,3,0,30,25,26,3,0,15},{[4]="comment"}} spec/samples/bad_code.lua (%d+) local A,B,C="package","embrace","hepler";return {{{"112",A,1,1,7,[23]={A,"loaded",true}},{"211","helper",3,16,21,[10]=true},{"212","...",3,23,25},{"111",B,7,10,16,[11]=true,[23]={B}},{"412","opt",8,10,12,7,18},{"113",C,9,11,16,[23]={C}}},{},{24,0,26,9,3,0,21,31,26,3,0},{[4]="comment"}} spec/samples/python_code.lua (%d+) return {{{"011",[3]=1,[4]=6,[5]=15,[12]="expected '=' near '__future__'"}},{},{}} ]]):gsub("[%[%]]", "%%%0"))) -- luacheck: pop format_version = tonumber(format_version) assert.number(format_version, "Cache string is:" .. cache) assert.string(good_mtime) assert.string(bad_mtime) assert.string(python_mtime) assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) local function write_new_cache(version) local fh = io.open(tmpname, "wb") assert.userdata(fh) fh:write(([[ %s spec/samples/python_code.lua %s return {{{"111", "global", 1, 1, [23]={"global"}}, {"321", "uninit", 6, 8}},{},{}} spec/samples/good_code.lua %s return {{{"011",[3]=5,[4]=7,[12]="this code is actually bad"}},{},{}} spec/samples/bad_code.lua %s return {{},{},{}}]]):format(version, python_mtime, good_mtime, tostring(tonumber(bad_mtime) - 1))) fh:close() end write_new_cache("\n"..tostring(format_version)) assert.equal(mocked_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua spec/samples/unused_code.lua --std=lua52 --no-config --cache "..tmpname)) assert.equal(mocked_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua spec/samples/unused_code.lua --std=lua52 --no-config --cache "..tmpname)) write_new_cache("\n"..tostring(format_version + 1)) assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) write_new_cache("") assert.equal(normal_output, get_output("spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --no-config --cache "..tmpname)) end) end) if not multithreading.has_lanes then pending("uses multithreading") else it("uses multithreading", function() assert.equal([[ Checking spec/samples/good_code.lua OK Checking spec/samples/bad_code.lua 5 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/python_code.lua 1 error spec/samples/python_code.lua:1:6: expected '=' near '__future__' Total: 5 warnings / 1 error in 3 files ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 -j2 --no-config") end) end it("allows using custom formatter", function() assert.equal([[Files: 2 Formatter: spec.formatters.custom_formatter Quiet: 1 Color: false Codes: true ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua --formatter spec.formatters.custom_formatter -q --codes --no-color --no-config") end) it("loads custom formatters relatively to project root", function() assert.equal([[Files: 2 Formatter: spec.formatters.custom_formatter Quiet: 1 Color: false Codes: true ]], get_output("samples/good_code.lua samples/bad_code.lua --formatter spec.formatters.custom_formatter -q --codes --no-color --no-config", "spec/")) end) it("has built-in TAP formatter", function() assert.equal([[ 1..7 ok 1 spec/samples/good_code.lua not ok 2 spec/samples/bad_code.lua:3:16: unused function 'helper' not ok 3 spec/samples/bad_code.lua:3:23: unused variable length argument not ok 4 spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' not ok 5 spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 not ok 6 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' not ok 7 spec/samples/python_code.lua:1:6: expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter TAP --no-config") assert.equal([[ 1..7 ok 1 spec/samples/good_code.lua not ok 2 spec/samples/bad_code.lua:3:16: (W211) unused function 'helper' not ok 3 spec/samples/bad_code.lua:3:23: (W212) unused variable length argument not ok 4 spec/samples/bad_code.lua:7:10: (W111) setting non-standard global variable 'embrace' not ok 5 spec/samples/bad_code.lua:8:10: (W412) variable 'opt' was previously defined as an argument on line 7 not ok 6 spec/samples/bad_code.lua:9:11: (W113) accessing undefined variable 'hepler' not ok 7 spec/samples/python_code.lua:1:6: (E011) expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter TAP --codes --no-config") end) it("has built-in JUnit formatter", function() assert.equal([[ ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter JUnit --no-config") end) it("has built-in simple warning-per-line formatter", function() assert.equal("", get_output "spec/samples/good_code.lua --std=lua52 --formatter plain --no-config") assert.equal([[ spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' spec/samples/python_code.lua:1:6: expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter plain --no-config") assert.equal([[ spec/samples/404.lua: I/O error (couldn't read: No such file or directory) ]], get_output "spec/samples/404.lua --formatter plain --no-config") assert.equal([[ spec/samples/bad_code.lua:3:16: (W211) unused function 'helper' spec/samples/bad_code.lua:3:23: (W212) unused variable length argument spec/samples/bad_code.lua:7:10: (W111) setting non-standard global variable 'embrace' spec/samples/bad_code.lua:8:10: (W412) variable 'opt' was previously defined as an argument on line 7 spec/samples/bad_code.lua:9:11: (W113) accessing undefined variable 'hepler' spec/samples/python_code.lua:1:6: (E011) expected '=' near '__future__' ]], get_output "spec/samples/good_code.lua spec/samples/bad_code.lua spec/samples/python_code.lua --std=lua52 --formatter plain --codes --no-config") end) it("provides version info", function() local output = get_output "--version" assert.truthy(output:match("^Luacheck: [%w%p ]+\nLua: [%w%p ]+\nLuaFileSystem: [%w%p ]+\nLuaLanes: [%w%p ]+\n$")) end) it("expands folders", function() assert.matches("^Total: %d+ warnings / %d+ errors in 23 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() assert.matches("^Total: %d+ warnings / %d+ errors in 2 files\n$", get_output("spec/samples -qqq --no-config --include-files " .. quote("**/*.rockspec"))) end) describe("config", function() describe("loading", function() it("uses .luacheckrc in current directory if possible", function() assert.equal(norm_output [[ Checking nested/ab.lua 1 warning nested/ab.lua:1:10: accessing undefined variable 'b' Checking nested/nested/abc.lua 2 warnings nested/nested/abc.lua:1:7: accessing undefined variable 'a' nested/nested/abc.lua:1:13: accessing undefined variable 'c' Total: 3 warnings / 0 errors in 2 files ]], get_output("nested", "spec/configs/project/")) end) it("does not use .luacheckrc in current directory with --no-config", function() assert.equal(norm_output [[ Checking nested/ab.lua 2 warnings nested/ab.lua:1:7: accessing undefined variable 'a' nested/ab.lua:1:10: accessing undefined variable 'b' Checking nested/nested/abc.lua 3 warnings nested/nested/abc.lua:1:7: accessing undefined variable 'a' nested/nested/abc.lua:1:10: accessing undefined variable 'b' nested/nested/abc.lua:1:13: accessing undefined variable 'c' Total: 5 warnings / 0 errors in 2 files ]], get_output("nested --no-config", "spec/configs/project/")) end) it("uses .luacheckrc in upper directory", function() assert.equal(norm_output [[ Checking ab.lua 1 warning ab.lua:1:10: accessing undefined variable 'b' Checking nested/abc.lua 2 warnings nested/abc.lua:1:7: accessing undefined variable 'a' nested/abc.lua:1:13: accessing undefined variable 'c' Total: 3 warnings / 0 errors in 2 files ]], get_output("ab.lua nested", "spec/configs/project/nested/")) end) it("uses config provided with --config=path", function() assert.equal([[ Checking spec/samples/compat.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --config=spec/configs/global_config.luacheckrc") end) it("uses config when checking stdin", function() assert.equal([[ Checking stdin OK Total: 0 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/global_config.luacheckrc < spec/samples/compat.lua") end) it("uses per-file overrides", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/unused_code.lua OK Total: 4 warnings / 0 errors in 2 files ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc") end) it("uses new filename when selecting per-file overrides", function() assert.equal([[ Checking spec/samples/unused_code.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "- --config=spec/configs/override_config.luacheckrc --filename spec/samples/unused_code.lua < spec/samples/unused_code.lua") end) it("uses all overrides prefixing file name", function() assert.equal([[ Checking spec/samples/unused_secondaries.lua 1 warning spec/samples/unused_secondaries.lua:12:1: value assigned to variable 'o' is unused Checking spec/samples/unused_code.lua 7 warnings spec/samples/unused_code.lua:3:18: unused argument 'baz' spec/samples/unused_code.lua:4:8: unused loop variable 'i' spec/samples/unused_code.lua:7:11: unused loop variable 'a' spec/samples/unused_code.lua:7:14: unused loop variable 'b' spec/samples/unused_code.lua:7:17: unused loop variable 'c' spec/samples/unused_code.lua:13:7: value assigned to variable 'x' is overwritten on line 14 before use spec/samples/unused_code.lua:14:1: value assigned to variable 'x' is overwritten on line 15 before use Total: 8 warnings / 0 errors in 2 files ]], get_output "spec/samples/unused_secondaries.lua spec/samples/unused_code.lua --config=spec/configs/multioverride_config.luacheckrc") end) it("allows reenabling warnings ignored in config using --enable", function() assert.equal([[ Checking spec/samples/bad_code.lua 4 warnings spec/samples/bad_code.lua:3:16: unused function 'helper' spec/samples/bad_code.lua:3:23: unused variable length argument spec/samples/bad_code.lua:7:10: setting non-standard global variable 'embrace' spec/samples/bad_code.lua:9:11: accessing undefined variable 'hepler' Checking spec/samples/unused_code.lua 1 warning spec/samples/unused_code.lua:5:13: unused variable 'q' Total: 5 warnings / 0 errors in 2 files ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/override_config.luacheckrc --enable=211") end) it("allows using cli-specific options in top level config", function() assert.equal([[Files: 2 Warnings: 14 Errors: 0 Quiet: 0 Color: false Codes: true ]], get_output "spec/samples/bad_code.lua spec/samples/unused_code.lua --config=spec/configs/cli_specific_config.luacheckrc --std=lua52") end) it("uses exclude_files option", function() assert.equal(([[ Checking spec/samples/argparse.lua 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 Checking spec/samples/globals.lua 2 warnings Checking spec/samples/indirect_globals.lua 3 warnings Checking spec/samples/inline_options.lua 7 warnings / 2 errors Checking spec/samples/line_length.lua 8 warnings Checking spec/samples/python_code.lua 1 error Checking spec/samples/read_globals.lua 5 warnings Checking spec/samples/read_globals_inline_options.lua 3 warnings Checking spec/samples/redefined.lua 7 warnings Checking spec/samples/unused_code.lua 9 warnings Checking spec/samples/unused_secondaries.lua 4 warnings Total: 67 warnings / 4 errors in 16 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 compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings Checking globals.lua 2 warnings Checking indirect_globals.lua 3 warnings Checking inline_options.lua 7 warnings / 2 errors Checking line_length.lua 8 warnings Checking python_code.lua 1 error Checking read_globals.lua 5 warnings Checking read_globals_inline_options.lua 3 warnings Checking redefined.lua 7 warnings Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings Total: 67 warnings / 4 errors in 16 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 compat.lua 4 warnings Checking custom_std_inline_options.lua 3 warnings / 1 error Checking global_inline_options.lua 3 warnings Checking globals.lua 2 warnings Checking indirect_globals.lua 3 warnings Checking inline_options.lua 7 warnings / 2 errors Checking line_length.lua 8 warnings Checking python_code.lua 1 error Checking redefined.lua 7 warnings Checking unused_code.lua 9 warnings Checking unused_secondaries.lua 4 warnings Total: 59 warnings / 4 errors in 14 files ]], get_output(". --config=spec/configs/exclude_files_config.luacheckrc -qq --exclude-files global_fields.lua --exclude-files " .. quote("./read*"), "spec/samples/")) end) it("allows defining custom stds", function() assert.equal([[ Checking spec/samples/globals.lua 2 warnings spec/samples/globals.lua:1:15: accessing undefined variable 'rawlen' spec/samples/globals.lua:1:22: accessing undefined variable 'tostring' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc") assert.equal([[ Checking spec/samples/globals.lua 2 warnings spec/samples/globals.lua:1:1: accessing undefined variable 'print' spec/samples/globals.lua:1:15: accessing undefined variable 'rawlen' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=other_std") assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:15: accessing undefined variable 'rawlen' Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=+other_std") assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:7: accessing undefined variable 'setfenv' Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/custom_stds_config.luacheckrc --std=lua52") end) it("allows importing options with require", function() assert.equal([[ Checking spec/samples/globals.lua 1 warning spec/samples/globals.lua:1:7: (W113) accessing undefined variable 'setfenv' Total: 1 warning / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/import_config.luacheckrc") end) describe("global path", function() setup(function() os.rename(".luacheckrc", ".luacheckrc.bak") end) teardown(function() os.rename(".luacheckrc.bak", ".luacheckrc") end) it("uses global path as fallback if --[no-]config is not used", function() assert.equal([[ Checking spec/samples/compat.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --default-config=spec/configs/global_config.luacheckrc") end) it("detects errors in global path config", function() assert.matches([[ Critical error: Couldn't load configuration from spec/configs/bad_config.luacheckrc: syntax error %(line 2: .* near 'method_missing'%) ]], get_output "spec/samples/compat.lua --default-config=spec/configs/bad_config.luacheckrc") end) it("does not use global path config if it is missing", function() assert.equal([[ Checking spec/samples/compat.lua 2 warnings spec/samples/compat.lua:1:2: accessing undefined variable 'setfenv' spec/samples/compat.lua:1:22: accessing undefined variable 'setfenv' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --std=lua52 --default-config=spec/configs/config_404.luacheckrc") end) it("does not use global path as fallback if --no-default-config is not used", function() assert.equal([[ Checking spec/samples/compat.lua 2 warnings spec/samples/compat.lua:1:2: accessing undefined variable 'setfenv' spec/samples/compat.lua:1:22: accessing undefined variable 'setfenv' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --std=lua52 --no-default-config") end) it("does not use global path as fallback if --config is used", function() assert.equal([[ Files: 1 Warnings: 2 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") end) it("does not use global path as fallback if --no-config is used", function() assert.equal([[ Checking spec/samples/compat.lua 2 warnings spec/samples/compat.lua:1:2: accessing undefined variable 'setfenv' spec/samples/compat.lua:1:22: accessing undefined variable 'setfenv' Total: 2 warnings / 0 errors in 1 file ]], get_output "spec/samples/compat.lua --std=lua52 --default-config=spec/configs/global_config.luacheckrc --no-config") end) end) end) describe("error handling", function() it("raises critical error on config with syntax errors", function() assert.matches([[ Critical error: Couldn't load configuration from spec/configs/bad_config.luacheckrc: syntax error %(line 2: .*%) ]], get_output "spec/samples/empty.lua --config=spec/configs/bad_config.luacheckrc") assert.equal(4, get_exitcode "spec/samples/empty.lua --config=spec/configs/bad_config.luacheckrc") end) it("raises critical error on non-existent config", function() assert.equal([[ Critical error: Couldn't find configuration file spec/configs/config_404.luacheckrc ]], get_output "spec/samples/empty.lua --config=spec/configs/config_404.luacheckrc") assert.equal(4, get_exitcode "spec/samples/empty.lua --config=spec/configs/config_404.luacheckrc") end) end) describe("overwriting", function() it("prioritizes CLI options over config", function() assert.equal(1, get_exitcode "spec/samples/compat.lua --config=spec/configs/cli_override_config.luacheckrc --std=min --new-globals foo") end) it("prioritizes CLI options over config overrides", function() assert.equal(1, get_exitcode "spec/samples/compat.lua --config=spec/configs/cli_override_file_config.luacheckrc --std=min --new-globals foo") end) it("concats array-like options from config and CLI", function() assert.equal([[ Checking spec/samples/globals.lua OK Total: 0 warnings / 0 errors in 1 file ]], get_output "spec/samples/globals.lua --config=spec/configs/global_config.luacheckrc --globals tostring") end) end) end) end) luacheck-0.21.1/spec/config_spec.lua000066400000000000000000000131661315524664100173040ustar00rootroot00000000000000local lfs = require "lfs" local config = require "luacheck.config" local fs = require "luacheck.fs" local P = fs.normalize local cur_dir = lfs.currentdir() local function nest(dir, func) local backed = false local function back() if not backed then lfs.chdir(cur_dir) backed = true end end finally(back) lfs.chdir(dir) func() back() end describe("config", function() it("has default path", function() assert.is_string(config.default_path) end) it("loads default config", function() local conf = config.load_config() assert.is_table(conf) nest("spec/configs", function() local nested_conf = config.load_config() assert.is_table(nested_conf) assert.same(config.get_top_options(conf), config.get_top_options(nested_conf)) assert.same(config.get_options(conf, P"spec/foo.lua"), config.get_options(nested_conf, P"../foo.lua")) assert.equal(P"../../bar.lua", config.relative_path(nested_conf, "bar.lua")) end) assert.not_same(config.get_options(conf, P"spec/foo_spec.lua"), config.get_options(conf, "foo_spec.lua")) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("works with empty config", function() local conf = config.empty_config assert.is_table(conf) assert.same({}, config.get_top_options(conf)) assert.same({}, config.get_options(conf, "bar.lua")) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("loads config from path", function() local conf = config.load_config(P"spec/configs/override_config.luacheckrc") assert.is_table(conf) nest("spec/configs/project", function() local nested_conf = config.load_config(P"spec/configs/override_config.luacheckrc") assert.is_table(nested_conf) assert.same(config.get_top_options(conf), config.get_top_options(nested_conf)) assert.same( config.get_options(conf, P"spec/samples/bad_code.lua"), config.get_options(nested_conf, P"../../samples/bad_code.lua") ) assert.equal(P"../../../bar.lua", config.relative_path(nested_conf, "bar.lua")) end) assert.not_same( config.get_options(conf, P"spec/samples/bad_code.lua"), config.get_options(conf, P"spec/samples/unused_code.lua") ) assert.equal("bar.lua", config.relative_path(conf, "bar.lua")) end) it("returns nil, error on missing config", function() local conf, err = config.load_config(P"spec/configs/config_404.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't find configuration file "..P"spec/configs/config_404.luacheckrc", err) end) it("returns nil, error on config with bad syntax", function() local conf, err = config.load_config(P"spec/configs/bad_config.luacheckrc") assert.is_nil(conf) assert.matches("Couldn't load configuration from "..P"spec/configs/bad_config.luacheckrc".. ": syntax error %(line 2: .*%)", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config(P"spec/configs/bad_config.luacheckrc") assert.is_nil(nested_conf) assert.matches("Couldn't load configuration from "..P"../../../spec/configs/bad_config.luacheckrc".. ": syntax error %(line 2: .*%)", nested_err) end) end) it("returns nil, error on config with runtime issues", function() local conf, err = config.load_config(P"spec/configs/runtime_bad_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from "..P"spec/configs/runtime_bad_config.luacheckrc".. ": runtime error (line 1: attempt to call a nil value)", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config(P"spec/configs/runtime_bad_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from "..P"../../../spec/configs/runtime_bad_config.luacheckrc".. ": runtime error (line 1: attempt to call a nil value)", nested_err) end) end) it("returns nil, error on invalid config", function() local conf, err = config.load_config(P"spec/configs/invalid_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from "..P"spec/configs/invalid_config.luacheckrc".. ": invalid value of option 'ignore'", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config(P"spec/configs/invalid_config.luacheckrc") assert.is_nil(nested_conf) assert.equal("Couldn't load configuration from "..P"../../../spec/configs/invalid_config.luacheckrc".. ": invalid value of option 'ignore'", nested_err) end) end) it("returns nil, error on config with invalid override", function() local conf, err = config.load_config(P"spec/configs/invalid_override_config.luacheckrc") assert.is_nil(conf) assert.equal("Couldn't load configuration from "..P"spec/configs/invalid_override_config.luacheckrc".. ": invalid value of option 'enable' in options for path 'spec/foo.lua'", err) nest("spec/configs/project", function() local nested_conf, nested_err = config.load_config(P"spec/configs/invalid_override_config.luacheckrc") assert.is_nil(nested_conf) assert.equal( "Couldn't load configuration from "..P"../../../spec/configs/invalid_override_config.luacheckrc".. ": invalid value of option 'enable' in options for path 'spec/foo.lua'", nested_err ) end) end) end) luacheck-0.21.1/spec/configs/000077500000000000000000000000001315524664100157435ustar00rootroot00000000000000luacheck-0.21.1/spec/configs/bad_config.luacheckrc000066400000000000000000000001171315524664100220430ustar00rootroot00000000000000-- let's talk about ruby def method_missing(*args); args.join(" "); end -- wat luacheck-0.21.1/spec/configs/cli_override_config.luacheckrc000066400000000000000000000000671315524664100237670ustar00rootroot00000000000000global = true globals = {"print", "setfenv", "rawlen"} luacheck-0.21.1/spec/configs/cli_override_file_config.luacheckrc000066400000000000000000000001451315524664100247630ustar00rootroot00000000000000files["spec/samples/compat.lua"] = { global = true, globals = {"print", "setfenv", "rawlen"} } luacheck-0.21.1/spec/configs/cli_specific_config.luacheckrc000066400000000000000000000004621315524664100237340ustar00rootroot00000000000000color = false codes = true formatter = function(report, file_names, options) return ([[ Files: %d Warnings: %d Errors: %d Quiet: %d Color: %s Codes: %s]]):format(#file_names, report.warnings, report.errors, options.quiet, options.color and "true" or "false", options.codes and "true" or "false") end luacheck-0.21.1/spec/configs/config.lua000066400000000000000000000000341315524664100177100ustar00rootroot00000000000000return { std = "lua52" } luacheck-0.21.1/spec/configs/custom_fields_config.luacheckrc000066400000000000000000000006761315524664100241670ustar00rootroot00000000000000stds = { my_server = { read_globals = { server = { fields = { bar = {read_only = false}, baz = {}, sessions = {read_only = false, other_fields = true} } } } } } read_globals = { server = { fields = { bar = {}, baz = {read_only = false}, sessions = {read_only = false, other_fields = true} } } } luacheck-0.21.1/spec/configs/custom_stds_config.luacheckrc000066400000000000000000000002061315524664100236630ustar00rootroot00000000000000stds = { my_std = {read_globals = {"print", "setfenv"}}, other_std = {read_globals = {"tostring", "setfenv"}} } std = "my_std" luacheck-0.21.1/spec/configs/exclude_files_config.luacheckrc000066400000000000000000000001111315524664100241220ustar00rootroot00000000000000exclude_files = {"spec/samples/defined?.lua", "**/bad*.lua"} std = "min" luacheck-0.21.1/spec/configs/global_config.luacheckrc000066400000000000000000000000511315524664100225520ustar00rootroot00000000000000globals = {"print", "setfenv", "rawlen"} luacheck-0.21.1/spec/configs/import_config.luacheckrc000066400000000000000000000001011315524664100226200ustar00rootroot00000000000000codes = true std = "lua51" return require "spec.configs.config" luacheck-0.21.1/spec/configs/invalid_config.luacheckrc000066400000000000000000000000171315524664100227420ustar00rootroot00000000000000ignore = "211" luacheck-0.21.1/spec/configs/invalid_override_config.luacheckrc000066400000000000000000000000671315524664100246460ustar00rootroot00000000000000ignore = {"211"} files["spec/foo.lua"].enable = "211" luacheck-0.21.1/spec/configs/multioverride_config.luacheckrc000066400000000000000000000002461315524664100242120ustar00rootroot00000000000000unused = false files["spec/samples/"] = { unused_args = true, enable = {"31"}, ignore = {"213"} } files["spec/samples/*_code.lua"] = { enable = {"213"} } luacheck-0.21.1/spec/configs/override_config.luacheckrc000066400000000000000000000002061315524664100231330ustar00rootroot00000000000000files["spec/samples/bad_code.lua"] = { redefined = false, compat = true } files["spec/samples/unused_code.lua"].unused = false luacheck-0.21.1/spec/configs/project/000077500000000000000000000000001315524664100174115ustar00rootroot00000000000000luacheck-0.21.1/spec/configs/project/.luacheckrc000066400000000000000000000001641315524664100215170ustar00rootroot00000000000000files["nested/ab.lua"].ignore = {"a"} files["nested/nested/"].enable = {"a"} files["nested/nested/"].ignore = {"b"} luacheck-0.21.1/spec/configs/project/nested/000077500000000000000000000000001315524664100206735ustar00rootroot00000000000000luacheck-0.21.1/spec/configs/project/nested/ab.lua000066400000000000000000000000141315524664100217530ustar00rootroot00000000000000print(a, b) luacheck-0.21.1/spec/configs/project/nested/nested/000077500000000000000000000000001315524664100221555ustar00rootroot00000000000000luacheck-0.21.1/spec/configs/project/nested/nested/abc.lua000066400000000000000000000000171315524664100234030ustar00rootroot00000000000000print(a, b, c) luacheck-0.21.1/spec/configs/runtime_bad_config.luacheckrc000066400000000000000000000000101315524664100235760ustar00rootroot00000000000000(nil)() luacheck-0.21.1/spec/expand_rockspec_spec.lua000066400000000000000000000016651315524664100212100ustar00rootroot00000000000000local expand_rockspec = require "luacheck.expand_rockspec" describe("expand_rockspec", function() it("returns sorted array of lua files related to a rock", function() assert.same({ "bar.lua", "baz.lua", "bin.lua", "foo.lua" }, expand_rockspec("spec/folder/rockspec")) end) it("returns nil, \"I/O\" for non-existent paths", function() local ok, err = expand_rockspec("spec/folder/non-existent") assert.is_nil(ok) assert.equal("I/O", err) end) it("returns nil, \"syntax\" for rockspecs with syntax errors", function() local ok, err = expand_rockspec("spec/folder/bad_config") assert.is_nil(ok) assert.equal("syntax", err) end) it("returns nil, \"runtime\" for rockspecs with run-time errors", function() local ok, err = expand_rockspec("spec/folder/bad_rockspec") assert.is_nil(ok) assert.equal("runtime", err) end) end) luacheck-0.21.1/spec/filter_spec.lua000066400000000000000000000371721315524664100173270ustar00rootroot00000000000000local filter_full = require "luacheck.filter".filter local function filter(issue_arrays, opts) local report = {} for i, issues in ipairs(issue_arrays) do for issue_index, issue in ipairs(issues) do issue.line = issue_index end report[i] = {events = issues, per_line_options = {}, line_lengths = {}} end local result = filter_full(report, opts) for _, file_report in ipairs(result) do for _, issue in ipairs(file_report) do issue.line = nil end end return result end describe("filter", function() it("filters warnings by name", function() assert.same({ { { code = "211", name = "baz" } } }, filter({ { { code = "211", name = "foo" }, { code = "211", name = "bar" }, { code = "211", name = "baz" } } }, { ignore = {"bar"}, only = {"bar", "baz"} })) end) it("removes unused var/value and redefined warnings related to _, unless it's useless", function() assert.same({ { { code = "211", name = "foo" }, { code = "211", name = "_", useless = true } } }, filter({ { { code = "211", name = "foo" }, { code = "211", name = "_", useless = true }, { code = "412", name = "_" }, { code = "221", name = "_" } } })) end) it("filters warnings by type", function() assert.same({ { { code = "211", name = "foo" } } }, filter({ { { code = "211", name = "foo" }, { code = "111", name = "bar", indexing = {"bar"} }, { code = "413", name = "baz" } } }, { global = false, redefined = false })) assert.same({ { { code = "221", name = "foo" }, { code = "111", name = "bar", indexing = {"bar"} }, { code = "413", name = "baz" } } }, filter({ { { code = "221", name = "foo" }, { code = "111", name = "bar", indexing = {"bar"} }, { code = "321", name = "qu" }, { code = "413", name = "baz" } } }, { ignore = {"32"} })) end) it("filters warnings by code and name using patterns", function() assert.same({ { { code = "212", name = "bar" }, { code = "413", name = "_baz" } } }, filter({ { { code = "212", name = "bar" }, { code = "212", name = "_qu" }, { code = "321", name = "foo" }, { code = "413", name = "_baz" } } }, { ignore = {"foo", "212/_.*"} })) end) it("filters unused warnings by subtype", function() assert.same({ { { code = "211", name = "foo" } } }, filter({ { { code = "211", name = "foo" }, { code = "311", name = "bar" }, { code = "212", name = "baz" }, { code = "221", name = "qu" } } }, { ignore = {"22", "31"}, unused_args = false })) end) it("filters unused warnings related to secondary variables", function() assert.same({ { { code = "212", name = "baz" } } }, filter({ { { code = "211", name = "foo", secondary = true }, { code = "311", name = "bar", secondary = true }, { code = "212", name = "baz" } } }, { unused_secondaries = false })) end) it("filters unused and redefined warnings related to implicit self", function() assert.same({ { { code = "212", name = "self" } } }, filter({ { { code = "212", name = "self", self = true }, { code = "432", name = "self", self = true }, { code = "212", name = "self" } } }, { self = false })) end) it("filters defined globals", function() assert.same({ { { code = "111", name = "module", indexing = {"module"} } } }, filter({ { { code = "113", name = "foo", indexing = {"foo"} }, { code = "111", name = "module", indexing = {"module"} } } }, { std = {}, globals = {"foo"} })) end) it("filters standard globals", function() assert.same({ { { code = "111", name = "module", indexing = {"module"} } } }, filter({ { { code = "113", name = "package", indexing = {"package"} }, { code = "111", name = "module", indexing = {"module"} } } }, { std = "min" })) end) it("allows defined globals with allow_defined = true", function() assert.same({ { { code = "131", name = "bar", indexing = {"bar"} }, { code = "113", name = "baz", indexing = {"baz"} } } }, filter({ { { code = "113", name = "foo" }, { code = "111", name = "foo" }, { code = "111", name = "bar", indexing = {"bar"} }, { code = "113", name = "baz", indexing = {"baz"} } } }, { allow_defined = true })) end) it("allows globals defined in top level function scope with allow_defined_top = true", function() assert.same({ { { code = "111", name = "bar", indexing = {"bar"} }, { code = "113", name = "baz", indexing = {"baz"} } } }, filter({ { { code = "113", name = "foo", indexing = {"foo"} }, { code = "111", name = "foo", indexing = {"foo"}, top = true }, { code = "111", name = "bar", indexing = {"bar"} }, { code = "113", name = "baz", indexing = {"baz"} } } }, { allow_defined_top = true })) end) it("allows globals defined in the same file with module = true", function() assert.same({ {}, { { code = "113", name = "foo", indexing = {"foo"} } } }, filter({ { { code = "113", name = "foo", indexing = {"foo"} }, { code = "111", name = "foo", indexing = {"foo"} } }, { { code = "113", name = "foo", indexing = {"foo"} } } }, { allow_defined = true, module = true })) end) it("only allows setting globals defined in the same file with module = true", function() assert.same({ {}, { { code = "111", name = "string", indexing = {"string"}, module = true }, { code = "111", name = "bar", indexing = {"bar"}, module = true } } }, filter({ { { code = "111", name = "bar", indexing = {"bar"} } }, { { code = "111", name = "foo", indexing = {"foo"}, top = true }, { code = "111", name = "foo", indexing = {"foo"} }, { code = "111", name = "string", indexing = {"string"} }, { code = "111", name = "bar", indexing = {"bar"} } } }, { { allow_defined = true, ignore = {"13"} }, { allow_defined_top = true, module = true } })) end) it("using an implicitly defined global from a module marks it as used", function() assert.same({ {}, {} }, filter({ { { code = "111", name = "foo", indexing = {"foo"} } }, { { code = "113", name = "foo", indexing = {"foo"} }, { code = "111", name = "bar", indexing = {"bar"} } } }, { { allow_defined = true }, { allow_defined = true, module = true } })) end) 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 = "021", msg = "invalid value of inline option 'std'", line = 8, column = 1}, {code = "021", msg = "invalid value of inline option 'std'", line = 1000, column = 20} } }, 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} } }, 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}, {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} } }, line_lengths = {} } }, { { std = "max", inline = false } })) end) it("adds line length warnings", function() assert.same({ { {code = "631", line = 2, column = 121, end_column = 121, max_length = 120}, {code = "631", line = 5, column = 16, end_column = 18, line_ending = "string", max_length = 15} } }, filter_full({ { events = { {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"} } }, {})) end) end) luacheck-0.21.1/spec/folder/000077500000000000000000000000001315524664100155665ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/bad_config000066400000000000000000000000141315524664100175570ustar00rootroot00000000000000foo = `bar` luacheck-0.21.1/spec/folder/bad_rockspec000066400000000000000000000000201315524664100201200ustar00rootroot00000000000000build "builtin" luacheck-0.21.1/spec/folder/bom000066400000000000000000000000131315524664100162600ustar00rootroot00000000000000foo bar luacheck-0.21.1/spec/folder/config000066400000000000000000000000141315524664100167510ustar00rootroot00000000000000foo = "bar" luacheck-0.21.1/spec/folder/env_config000066400000000000000000000000141315524664100176210ustar00rootroot00000000000000foo = bar() luacheck-0.21.1/spec/folder/folder1/000077500000000000000000000000001315524664100171225ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/folder1/another000066400000000000000000000000001315524664100204730ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/folder1/fail000066400000000000000000000000001315524664100177460ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/folder1/file000066400000000000000000000000001315524664100177520ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/folder2/000077500000000000000000000000001315524664100171235ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/folder2/garbage000066400000000000000000000000001315524664100204240ustar00rootroot00000000000000luacheck-0.21.1/spec/folder/foo000066400000000000000000000000111315524664100162640ustar00rootroot00000000000000contents luacheck-0.21.1/spec/folder/rockspec000066400000000000000000000003511315524664100173210ustar00rootroot00000000000000build = { type = "builtin", modules = { foo = "foo.lua", bar = "bar.lua", qu = "qu.c" }, install = { lua = { baz = "baz.lua" }, bin = { bin = "bin.lua" } } } luacheck-0.21.1/spec/format_spec.lua000066400000000000000000000106611315524664100173240ustar00rootroot00000000000000local format = require "luacheck.format".format local function mark_colors(s) return (s:gsub("\27%[%d+m", "\27"):gsub("\27+", "#")) end describe("format", function() it("returns formatted report", function() assert.equal([[Checking stdin 1 warning stdin:2:7: unused global variable 'foo' Checking foo.lua 1 warning foo.lua:2:7: empty statement Checking bar.lua OK Checking baz.lua 1 error baz.lua:4:3: something went wrong Total: 2 warnings / 1 error in 4 files]], format({ warnings = 2, errors = 1, fatals = 0, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "551", line = 2, column = 7 } }, {}, { { code = "011", line = 4, column = 3, msg = "something went wrong" } } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {color = false})) end) it("does not output OK messages with options.quiet >= 1", function() assert.equal([[Checking stdin 1 warning stdin:2:7: unused global variable 'foo' Checking foo.lua 1 warning / 1 error foo.lua:2:7: unused global variable 'foo' foo.lua:3:10: bad, bad inline option Checking baz.lua Syntax error baz.lua: error message Total: 2 warnings / 1 error in 3 files, couldn't check 1 file]], format({ warnings = 2, errors = 1, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 }, { code = "021", msg = "bad, bad inline option", line = 3, column = 10 } }, {}, { fatal = "syntax", msg = "error message" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 1, color = false})) end) it("does not output warnings with options.quiet >= 2", function() assert.equal([[Checking stdin 1 warning Checking foo.lua 1 warning Checking baz.lua Syntax error Total: 2 warnings / 0 errors in 3 files, couldn't check 1 file]], format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 2, color = false})) end) it("does not output file info with options.quiet == 3", function() assert.equal("Total: 2 warnings / 0 errors in 3 files, couldn't check 1 file", format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {quiet = 3, color = false})) end) it("colors output by default", function() if package.config:sub(1, 1) == "\\" and not os.getenv("ANSICON") then pending("uses terminal colors") end assert.equal([[Checking stdin #1 warning# stdin:2:7: unused global variable #foo# Checking foo.lua #1 warning# foo.lua:2:7: unused global variable #foo# Checking bar.lua #OK# Checking baz.lua #Syntax error# baz.lua: error message Total: #2# warnings / #0# errors in 3 files, couldn't check 1 file]], mark_colors(format({ warnings = 2, errors = 0, fatals = 1, { { code = "131", name = "foo", line = 2, column = 7 } }, { { code = "131", name = "foo", line = 2, column = 7 } }, {}, { fatal = "syntax", msg = "error message" } }, {"stdin", "foo.lua", "bar.lua", "baz.lua"}, {}))) end) end) luacheck-0.21.1/spec/formatters/000077500000000000000000000000001315524664100165015ustar00rootroot00000000000000luacheck-0.21.1/spec/formatters/custom_formatter.lua000066400000000000000000000003431315524664100226010ustar00rootroot00000000000000return function(report, file_names, options) return ([[ Files: %d Formatter: %s Quiet: %d Color: %s Codes: %s]]):format(#file_names, options.formatter, options.quiet, tostring(options.color), tostring(options.codes)) end luacheck-0.21.1/spec/fs_spec.lua000066400000000000000000000103761315524664100164470ustar00rootroot00000000000000local lfs = require "lfs" local fs = require "luacheck.fs" local utils = require "luacheck.utils" local P = fs.normalize describe("fs", function() describe("is_dir", function() it("returns true for directories", function() assert.is_true(fs.is_dir("spec/folder")) end) it("returns false for files", function() assert.is_false(fs.is_dir("spec/folder/foo")) end) it("returns false for non-existent paths", function() assert.is_false(fs.is_dir("spec/folder/non-existent")) end) end) describe("is_file", function() it("returns true for files", function() assert.is_true(fs.is_file("spec/folder/foo")) end) it("returns false for directories", function() assert.is_false(fs.is_file("spec/folder")) end) it("returns false for non-existent paths", function() assert.is_false(fs.is_file("spec/folder/non-existent")) end) end) describe("extract_files", function() it("returns sorted list of files in a directory matching pattern", function() assert.same({ P"spec/folder/folder1/fail", P"spec/folder/folder1/file", P"spec/folder/foo" }, fs.extract_files(P"spec/folder", "^f")) end) end) describe("get_mtime", function() it("returns modification time as a number", function() assert.number(fs.get_mtime("spec/folder/foo")) end) it("returns nil for non-existent files", function() assert.is_nil(fs.get_mtime("spec/folder/non-existent")) end) end) describe("get_current_dir", function() it("returns absolute path to current directory with trailing directory separator", function() local current_dir = fs.get_current_dir() assert.string(current_dir) assert.matches(utils.dir_sep .. "$", current_dir) assert.not_equal("", (fs.split_base(current_dir))) assert.is_true(fs.is_file(current_dir .. "spec/folder/foo")) end) end) describe("find_file", function() it("finds file in a directory", function() local path = fs.get_current_dir() .. P"spec/folder" assert.equal(path, fs.find_file(path, "foo")) end) it("finds file in a parent directory", function() local path = fs.get_current_dir() .. P"spec/folder" assert.equal(path, fs.find_file(fs.join(path, "folder1"), "foo")) end) it("returns nil if can't find file", function() assert.is_nil( fs.find_file(fs.get_current_dir(), "this file shouldn't exist or it will make luacheck testsuite break")) 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 luacheck-0.21.1/spec/globbing_spec.lua000066400000000000000000000056701315524664100176230ustar00rootroot00000000000000local globbing = require "luacheck.globbing" describe("globbing", function() describe("match", function() it("returns true on literal match", function() assert.is_true(globbing.match("foo/bar", "foo/bar")) end) it("returns true on literal match after normalization", function() assert.is_true(globbing.match("foo//bar/baz/..", "./foo/bar/")) end) it("returns false for on literal mismatch", function() assert.is_false(globbing.match("foo/bar", "foo/baz")) end) it("accepts subdirectory matches", function() assert.is_true(globbing.match("foo/bar", "foo/bar/baz")) end) it("understands wildcards", function() assert.is_true(globbing.match("*", "foo")) assert.is_true(globbing.match("foo/*r", "foo/bar")) assert.is_true(globbing.match("foo/*r", "foo/bar/baz")) assert.is_false(globbing.match("foo/*r", "foo/baz")) end) it("understands optional characters", function() assert.is_false(globbing.match("?", "foo")) assert.is_true(globbing.match("???", "foo")) assert.is_true(globbing.match("????", "foo")) assert.is_true(globbing.match("f?o/?a?", "foo/bar")) assert.is_false(globbing.match("f?o/?a?", "foo/abc")) end) it("understands ranges and classes", function() assert.is_true(globbing.match("[d-h]o[something]", "foo")) assert.is_false(globbing.match("[d-h]o[somewhere]", "bar")) assert.is_false(globbing.match("[.-h]o[i-z]", "bar")) end) it("accepts closing bracket as first class character", function() assert.is_true(globbing.match("[]]", "]")) assert.is_false(globbing.match("[]]", "[")) assert.is_true(globbing.match("[]foo][]foo][]foo]", "foo")) end) it("accepts dash as first or last class character", function() assert.is_true(globbing.match("[-]", "-")) assert.is_false(globbing.match("[-]", "+")) assert.is_true(globbing.match("[---]", "-")) end) it("understands negation", function() assert.is_true(globbing.match("[!foo][!bar][!baz]", "boo")) assert.is_false(globbing.match("[!foo][!bar][!baz]", "far")) assert.is_false(globbing.match("[!a-z]", "g")) end) it("understands recursive globbing using **", function() assert.is_true(globbing.match("**/*.lua", "foo.lua")) assert.is_true(globbing.match("**/*.lua", "foo/bar.lua")) assert.is_false(globbing.match("foo/**/*.lua", "bar.lua")) assert.is_false(globbing.match("foo/**/*.lua", "foo.lua")) assert.is_true(globbing.match("foo/**/bar/*.lua", "foo/bar/baz.lua")) assert.is_true(globbing.match("foo/**/bar/*.lua", "foo/foo2/foo3/bar/baz.lua")) assert.is_false(globbing.match("foo/**/bar/*.lua", "foo/baz.lua")) assert.is_false(globbing.match("foo/**/bar/*.lua", "bar/baz.lua")) end) end) end) luacheck-0.21.1/spec/helper.lua000066400000000000000000000024271315524664100163020ustar00rootroot00000000000000local helper = {} local dir_sep = package.config:sub(1, 1) -- Return path to root directory when run from `path`. local function antipath(path) local _, level = path:gsub("[/\\]", "") return (".."..dir_sep):rep(level) end function helper.luacov_config(prefix) return { statsfile = prefix.."luacov.stats.out", modules = { luacheck = "src/luacheck/init.lua", ["luacheck.*"] = "src" }, exclude = { "bin/luacheck$", "luacheck/argparse$" } } end local luacov = package.loaded["luacov.runner"] -- Returns command that runs `luacheck` executable from `loc_path`. function helper.luacheck_command(loc_path) loc_path = loc_path or "." local prefix = antipath(loc_path) local cmd = ("cd %s && %s"):format(loc_path, arg[-5] or "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( prefix, prefix, dir_sep, prefix, dir_sep, dir_sep) if luacov then -- Launch luacov. cmd = cmd..(' -e "require[[luacov.runner]](require[[spec.helper]].luacov_config([[%s]]))"'):format(prefix) end return ("%s %sbin%sluacheck.lua"):format(cmd, prefix, dir_sep) end return helper luacheck-0.21.1/spec/lexer_spec.lua000066400000000000000000000502471315524664100171570ustar00rootroot00000000000000local lexer = require "luacheck.lexer" local function get_tokens(source) local lexer_state = lexer.new_state(source) local tokens = {} repeat local token = {} token.token, token.token_value, token.line, token.column, token.offset = lexer.next_token(lexer_state) tokens[#tokens+1] = token until token.token == "eof" return tokens end local function get_token(source) local lexer_state = lexer.new_state(source) local token = {} token.token, token.token_value = lexer.next_token(lexer_state) return token end local function maybe_error(lexer_state) local ok, err, line, column, _, end_column = lexer.next_token(lexer_state) return not ok and {msg = err, line = line, column = column, end_column = end_column} end local function get_error(source) return maybe_error(lexer.new_state(source)) end local function get_last_error(source) local lexer_state = lexer.new_state(source) local err repeat err = maybe_error(lexer_state) until err return err end describe("lexer", function() describe("quote", function() it("quotes strings", function() assert.equal("'foo'", lexer.quote("foo")) end) it("escapes not printable characters", function() assert.equal([['\0\1foo \240bar\127\10']], lexer.quote("\0\1foo \240bar\127\n")) end) end) it("parses EOS correctly", function() assert.same({token = "eof"}, get_token(" ")) end) it("parses names correctly", function() assert.same({token = "name", token_value = "foo"}, get_token("foo")) assert.same({token = "name", token_value = "_"}, get_token("_")) assert.same({token = "name", token_value = "foo1_2"}, get_token("foo1_2")) assert.same({token = "name", token_value = "foo"}, get_token("foo!")) end) it("parses keywords correctly", function() assert.same({token = "do"}, get_token("do")) assert.same({token = "goto"}, get_token("goto fail;")) end) it("parses operators and special tokens correctly", function() assert.same({token = "="}, get_token("= =")) assert.same({token = "=="}, get_token("==")) assert.same({token = "<"}, get_token("< =")) assert.same({token = "<="}, get_token("<=")) assert.same({token = "<<"}, get_token("<<")) assert.same({token = ">"}, get_token("> =")) assert.same({token = ">="}, get_token(">=")) assert.same({token = ">>"}, get_token(">>")) assert.same({token = "/"}, get_token("/ /")) assert.same({token = "//"}, get_token("//")) assert.same({token = "."}, get_token(".?.")) assert.same({token = "."}, get_token(".")) assert.same({token = ".."}, get_token("..%")) assert.same({token = "...", token_value = "..."}, get_token("...")) assert.same({token = ":"}, get_token(":.:")) assert.same({token = "::"}, get_token("::.")) end) it("parses single character tokens correctly", function() assert.same({token = "("}, get_token("((")) assert.same({token = "["}, get_token("[x]")) assert.same({token = "$"}, get_token("$$$")) end) describe("when parsing short strings", function() it("parses empty short strings correctly", function() assert.same({token = "string", token_value = ""}, get_token([[""]])) assert.same({token = "string", token_value = ""}, get_token([['']])) end) it("parses short strings containing quotation marks correctly", function() assert.same({token = "string", token_value = "'"}, get_token([["'"]])) assert.same({token = "string", token_value = '"'}, get_token([['"']])) end) it("parses simple short strings correctly", function() assert.same({token = "string", token_value = "foo"}, get_token([["foo"]])) end) it("parses simple escape sequences correctly", function() assert.same({token = "string", token_value = "\r\n"}, get_token([["\r\n"]])) assert.same({token = "string", token_value = "foo\\bar"}, get_token([["foo\\bar"]])) assert.same({token = "string", token_value = "a\'\'b\"\""}, get_token([["a\'\'b\"\""]])) end) it("parses escaped newline correctly", function() assert.same({token = "string", token_value = "foo \nbar"}, get_token([["foo \ bar"]])) assert.same({token = "string", token_value = "foo \n\n\nbar"}, get_token([["foo \ \ \ bar"]])) end) it("parses \\z correctly", function() assert.same({token = "string", token_value = "foo "}, get_token([["foo \z"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \zbar"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \z bar"]])) -- luacheck: ignore 613 assert.same({token = "string", token_value = "foo bar"}, get_token([["foo \z bar\z "]])) end) it("parses decimal escape sequences correctly", function() assert.same({token = "string", token_value = "\0buffer exploit"}, get_token([["\0buffer exploit"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo b\97r"]])) assert.same({token = "string", token_value = "\1234"}, get_token([["\1234"]])) assert.same( {line = 1, column = 2, end_column = 5, msg = "invalid decimal escape sequence '\\300'"}, get_error([["\300"]]) ) assert.same({line = 1, column = 2, end_column = 2, msg = "invalid escape sequence '\\'"}, get_error([["\]])) end) it("parses hexadecimal escape sequences correctly", function() assert.same({token = "string", token_value = "\0buffer exploit"}, get_token([["\x00buffer exploit"]])) assert.same({token = "string", token_value = "foo bar"}, get_token([["foo\x20bar"]])) assert.same({token = "string", token_value = "jj"}, get_token([["\x6a\x6A"]])) assert.same( {line = 1, column = 2, end_column = 3, msg = "invalid escape sequence '\\X'"}, get_error([["\XFF"]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\x\"'"}, get_error([["\x"]]) ) assert.same( {line = 1, column = 2, end_column = 5, msg = "invalid hexadecimal escape sequence '\\x1\"'"}, get_error([["\x1"]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\x1'"}, get_error([["\x1]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid hexadecimal escape sequence '\\xx'"}, get_error([["\xxx"]]) ) end) it("parses utf-8 escape sequences correctly", function() assert.same({token = "string", token_value = "\0\0"}, get_token([["\u{0}\u{00000000}"]])) assert.same({token = "string", token_value = "\0\127"}, get_token([["\u{0}\u{7F}"]])) assert.same({token = "string", token_value = "\194\128\223\191"}, get_token([["\u{80}\u{7fF}"]])) assert.same({token = "string", token_value = "\224\160\128\239\191\191"}, get_token([["\u{800}\u{FFFF}"]])) assert.same({token = "string", token_value = "\240\144\128\128\244\143\191\191"}, get_token([["\u{10000}\u{10FFFF}"]])) assert.same( {line = 1, column = 2, end_column = 10, msg = "invalid UTF-8 escape sequence '\\u{110000'"}, get_error([["\u{110000}"]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\u\"'"}, get_error([["\u"]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\un'"}, get_error([["\unrelated"]]) ) assert.same( {line = 1, column = 2, end_column = 7, msg = "invalid UTF-8 escape sequence '\\u{11u'"}, get_error([["\u{11unrelated"]]) ) assert.same( {line = 1, column = 2, end_column = 6, msg = "invalid UTF-8 escape sequence '\\u{11'"}, get_error([["\u{11]]) ) assert.same( {line = 1, column = 2, end_column = 5, msg = "invalid UTF-8 escape sequence '\\u{u'"}, get_error([["\u{unrelated}"]]) ) assert.same( {line = 1, column = 2, end_column = 4, msg = "invalid UTF-8 escape sequence '\\u{'"}, get_error([["\u{]]) ) end) it("detects unknown escape sequences", function() assert.same({line = 1, column = 2, end_column = 3, msg = "invalid escape sequence '\\c'"}, get_error([["\c"]])) end) it("detects unfinished strings", function() assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([["]])) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([["']])) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished string"}, get_error([[" "]])) end) end) describe("when parsing long strings", function() it("parses empty long strings correctly", function() assert.same({token = "string", token_value = ""}, get_token("[[]]")) assert.same({token = "string", token_value = ""}, get_token("[===[]===]")) end) it("parses simple long strings correctly", function() assert.same({token = "string", token_value = "foo"}, get_token("[[foo]]")) assert.same({token = "string", token_value = "'foo'\n'bar'\n"}, get_token("[===['foo'\n'bar'\n]===]")) end) it("skips first newline", function() assert.same({token = "string", token_value = ""}, get_token("[[\n]]")) assert.same({token = "string", token_value = "\n"}, get_token("[===[\n\n]===]")) end) it("ignores closing brackets of unrelated length", function() assert.same({token = "string", token_value = "]=] "}, get_token("[[]=] ]]")) assert.same({token = "string", token_value = "foo]]\n]=== ]]"}, get_token("[===[foo]]\n]=== ]]]===]")) end) it("detects invalid opening brackets", function() assert.same({line = 1, column = 1, end_column = 1, msg = "invalid long string delimiter"}, get_error("[=")) assert.same({line = 1, column = 1, end_column = 1, msg = "invalid long string delimiter"}, get_error("[=|")) end) it("detects unfinished long strings", function() assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long string"}, get_error("[=[\n")) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long string"}, get_error("[[]")) end) end) describe("when parsing numbers", function() it("parses decimal integers correctly", function() assert.same({token = "number", token_value = "0"}, get_token("0")) assert.same({token = "number", token_value = "123456789"}, get_token("123456789")) end) it("parses hexadecimal integers correctly", function() assert.same({token = "number", token_value = "0x0"}, get_token("0x0")) assert.same({token = "number", token_value = "0X0"}, get_token("0X0")) assert.same({token = "number", token_value = "0xFfab"}, get_token("0xFfab")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x")) end) it("parses decimal floats correctly", function() assert.same({token = "number", token_value = "0.0"}, get_token("0.0")) assert.same({token = "number", token_value = "0."}, get_token("0.")) assert.same({token = "number", token_value = ".1234"}, get_token(".1234")) end) it("parses hexadecimal floats correctly", function() assert.same({token = "number", token_value = "0xf.A"}, get_token("0xf.A")) assert.same({token = "number", token_value = "0x9."}, get_token("0x9.")) assert.same({token = "number", token_value = "0x.b"}, get_token("0x.b")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x.")) end) it("parses decimal floats with exponent correctly", function() assert.same({token = "number", token_value = "1.8e1"}, get_token("1.8e1")) assert.same({token = "number", token_value = ".8e-1"}, get_token(".8e-1")) assert.same({token = "number", token_value = "1.E+20"}, get_token("1.E+20")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e-")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8E+")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8ee")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8e-e")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("1.8E+i")) end) it("parses hexadecimal floats with exponent correctly", function() assert.same({token = "number", token_value = "0x1.8p1"}, get_token("0x1.8p1")) assert.same({token = "number", token_value = "0x.8P-1"}, get_token("0x.8P-1")) assert.same({token = "number", token_value = "0x1.p+20"}, get_token("0x1.p+20")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p-")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8P+")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8pF")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p-F")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x1.8p+LL")) assert.same({line = 1, column = 1, end_column = 1, msg = "malformed number"}, get_error("0x.p1")) end) it("parses 64 bits cdata literals correctly", function() assert.same({token = "number", token_value = "1LL"}, get_token("1LL")) assert.same({token = "number", token_value = "1ll"}, get_token("1ll")) assert.same({token = "number", token_value = "1Ll"}, get_token("1Ll")) assert.same({token = "number", token_value = "1lL"}, get_token("1lL")) assert.same({token = "number", token_value = "1ULL"}, get_token("1ULL")) assert.same({token = "number", token_value = "1uLl"}, get_token("1uLl")) assert.same({token = "number", token_value = "1LLu"}, get_token("1LLu")) assert.same({token = "number", token_value = "1"}, get_token("1L")) assert.same({token = "number", token_value = "1LL"}, get_token("1LLG")) assert.same({token = "number", token_value = "1"}, get_token("1LUL")) assert.same({token = "number", token_value = "0x1LL"}, get_token("0x1LL")) assert.same({token = "number", token_value = "1.0"}, get_token("1.0LL")) end) it("parses complex cdata literals correctly", function() assert.same({token = "number", token_value = "1i"}, get_token("1i")) assert.same({token = "number", token_value = "1I"}, get_token("1I")) assert.same({token = "number", token_value = "1"}, get_token("1j")) assert.same({token = "number", token_value = "1LL"}, get_token("1LLi")) assert.same({token = "number", token_value = "0x1i"}, get_token("0x1i")) assert.same({token = "number", token_value = "0x1.0i"}, get_token("0x1.0i")) end) end) it("parses short comments correctly", function() assert.same({token = "comment", token_value = ""}, get_token("--")) assert.same({token = "comment", token_value = "foo"}, get_token("--foo\nbar")) assert.same({token = "comment", token_value = "["}, get_token("--[")) assert.same({token = "comment", token_value = "[=foo"}, get_token("--[=foo\nbar")) end) it("parses long comments correctly", function() assert.same({token = "comment", token_value = ""}, get_token("--[[]]")) assert.same({token = "comment", token_value = ""}, get_token("--[[\n]]")) assert.same({token = "comment", token_value = "foo\nbar"}, get_token("--[[foo\nbar]]")) assert.same({line = 1, column = 1, end_column = 1, msg = "unfinished long comment"}, get_error("--[=[]]")) end) it("provides correct location info", function() assert.same({ {token = "local", line = 1, column = 1, offset = 1}, {token = "function", line = 1, column = 7, offset = 7}, {token = "name", token_value = "foo", line = 1, column = 16, offset = 16}, {token = "(", line = 1, column = 19, offset = 19}, {token = "name", token_value = "bar", line = 1, column = 20, offset = 20}, {token = ")", line = 1, column = 23, offset = 23}, {token = "return", line = 2, column = 4, offset = 28}, {token = "name", token_value = "bar", line = 2, column = 11, offset = 35}, {token = ":", line = 2, column = 14, offset = 38}, {token = "name", token_value = "get_foo", line = 2, column = 15, offset = 39}, {token = "string", token_value = "long string\n", line = 2, column = 22, offset = 46}, {token = "end", line = 5, column = 1, offset = 66}, {token = "name", token_value = "print", line = 7, column = 1, offset = 71}, {token = "string", token_value = "123\n", line = 7, column = 7, offset = 77}, {token = "eof", line = 10, column = 1, offset = 105} }, get_tokens([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\z 2\z 3\n" ]])) end) it("provides correct location info for errors", function() assert.same({line = 7, column = 9, end_column = 10, msg = "invalid escape sequence '\\g'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\g 2\z 3\n" ]])) assert.same({line = 8, column = 9, end_column = 12, msg = "invalid decimal escape sequence '\\300'"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\ 2\300 3\n" ]])) assert.same({line = 8, column = 1, end_column = 1, msg = "malformed number"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print ( 0xx) ]])) assert.same({line = 7, column = 7, end_column = 7, msg = "unfinished string"}, get_last_error([[ local function foo(bar) return bar:get_foo[=[ long string ]=] end print "1\z 2\z 3\n ]])) end) it("parses minified source correctly", function() assert.same({ {token = "name", token_value = "a", line = 1, column = 1, offset = 1}, {token = ",", line = 1, column = 2, offset = 2}, {token = "name", token_value = "b", line = 1, column = 3, offset = 3}, {token = "=", line = 1, column = 4, offset = 4}, {token = "number", token_value = "4ll", line = 1, column = 5, offset = 5}, {token = "name", token_value = "f", line = 1, column = 8, offset = 8}, {token = "=", line = 1, column = 9, offset = 9}, {token = "string", token_value = "", line = 1, column = 10, offset = 10}, {token = "function", line = 1, column = 12, offset = 12}, {token = "name", token_value = "_", line = 1, column = 21, offset = 21}, {token = "(", line = 1, column = 22, offset = 22}, {token = ")", line = 1, column = 23, offset = 23}, {token = "return", line = 1, column = 24, offset = 24}, {token = "number", token_value = "1", line = 1, column = 31, offset = 31}, {token = "or", line = 1, column = 32, offset = 32}, {token = "string", token_value = "", line = 1, column = 34, offset = 34}, {token = "end", line = 1, column = 36, offset = 36}, {token = "eof", line = 1, column = 39, offset = 39} }, get_tokens("a,b=4llf=''function _()return 1or''end")) end) it("handles argparse sample", function() get_tokens(io.open("spec/samples/argparse.lua", "rb"):read("*a")) end) end) luacheck-0.21.1/spec/linearize_spec.lua000066400000000000000000000223301315524664100200120ustar00rootroot00000000000000local linearize = require "luacheck.linearize" local parser = require "luacheck.parser" local utils = require "luacheck.utils" local ChState = utils.class() function ChState.__init() end function ChState.warn_redefined() end function ChState.warn_global() end function ChState.warn_unused_label() end function ChState.warn_unbalanced() end function ChState.warn_empty_block() end local function get_line_(src) local ast = parser.parse(src) local chstate = ChState() return linearize(chstate, ast) end local function get_line(src) local ok, res = pcall(get_line_, src) if ok or type(res) == "table" then return res else error(res, 0) end end local function item_to_string(item) if item.tag == "Jump" or item.tag == "Cjump" then return item.tag .. " -> " .. tostring(item.to) elseif item.tag == "Eval" then return "Eval " .. item.expr.tag elseif item.tag == "Local" then local buf = {} for _, node in ipairs(item.lhs) do table.insert(buf, ("%s (%d..%d)"):format(node.var.name, node.var.scope_start, node.var.scope_end)) end return "Local " .. table.concat(buf, ", ") else return item.tag end end local function get_line_as_string(src) local line = get_line(src) local buf = {} for i, item in ipairs(line.items) do buf[i] = tostring(i) .. ": " .. item_to_string(item) end return table.concat(buf, "\n") end local function value_info_to_string(item) local buf = {} for var, value in pairs(item.set_variables) do table.insert(buf, ("%s (%s / %s%s%s%s)"):format( var.name, var.type, value.type, value.empty and ", empty" or (value.initial and ", initial" or ""), value.secondaries and (", " .. tostring(#value.secondaries) .. " secondaries") or "", value.secondaries and value.secondaries.used and ", used" or "")) end table.sort(buf) return item.tag .. ": " .. table.concat(buf, ", ") end local function get_value_info_as_string(src) local line = get_line(src) local buf = {} for _, item in ipairs(line.items) do if item.tag == "Local" or item.tag == "Set" then assert.is_table(item.set_variables) table.insert(buf, value_info_to_string(item)) end end return table.concat(buf, "\n") end describe("linearize", function() describe("when handling post-parse syntax errors", function() it("detects gotos without labels", function() assert.same({line = 1, column = 1, end_column = 4, msg = "no visible label 'fail'"}, get_line("goto fail")) end) it("detects break outside loops", function() assert.same({line = 1, column = 1, end_column = 5, msg = "'break' is not inside a loop"}, get_line("break")) assert.same({line = 1, column = 28, end_column = 32, msg = "'break' is not inside a loop"}, get_line("while true do function f() break end end")) end) it("detects duplicate labels", function() assert.same({line = 2, column = 1, end_column = 8, msg = "label 'fail' already defined on line 1"}, get_line("::fail::\n::fail::")) end) it("detects varargs outside vararg functions", function() assert.same({line = 1, column = 21, end_column = 23, msg = "cannot use '...' outside a vararg function"}, get_line("function f() return ... end")) assert.same({line = 1, column = 42, end_column = 44, msg = "cannot use '...' outside a vararg function"}, get_line("function f(...) return function() return ... end end")) end) end) describe("when linearizing flow", function() it("linearizes empty source correctly", function() assert.equal("1: Local ... (2..1)", get_line_as_string("")) end) it("linearizes do-end blocks correctly", function() assert.equal([[ 1: Local ... (2..4) 2: Noop 3: Noop 4: Eval Call]], get_line_as_string([[ do end do print(foo) end]])) end) it("linearizes loops correctly", function() assert.equal([[ 1: Local ... (2..8) 2: Noop 3: Eval Id 4: Cjump -> 9 5: Local s (6..6) 6: Eval Call 7: Noop 8: Jump -> 3]], get_line_as_string([[ while cond do local s = io.read() print(s) end]])) assert.equal([[ 1: Local ... (2..6) 2: Noop 3: Local s (4..5) 4: Eval Call 5: Eval Id 6: Cjump -> 3]], get_line_as_string([[ repeat local s = io.read() print(s) until cond]])) assert.equal([[ 1: Local ... (2..9) 2: Noop 3: Eval Number 4: Eval Op 5: Cjump -> 10 6: Local i (7..7) 7: Eval Call 8: Noop 9: Jump -> 5]], get_line_as_string([[ for i = 1, #t do print(t[i]) end]])) assert.equal([[ 1: Local ... (2..8) 2: Noop 3: Eval Call 4: Cjump -> 9 5: Local k (6..6), v (6..6) 6: Eval Call 7: Noop 8: Jump -> 4]], get_line_as_string([[ for k, v in pairs(t) do print(k, v) end]])) end) it("linearizes loops with literal condition correctly", function() assert.equal([[ 1: Local ... (2..6) 2: Noop 3: Eval Number 4: Eval Call 5: Noop 6: Jump -> 3]], get_line_as_string([[ while 1 do foo() end]])) assert.equal([[ 1: Local ... (2..7) 2: Noop 3: Eval False 4: Jump -> 8 5: Eval Call 6: Noop 7: Jump -> 3]], get_line_as_string([[ while false do foo() end]])) assert.equal([[ 1: Local ... (2..4) 2: Noop 3: Eval Call 4: Eval True]], get_line_as_string([[ repeat foo() until true]])) assert.equal([[ 1: Local ... (2..5) 2: Noop 3: Eval Call 4: Eval Nil 5: Jump -> 3]], get_line_as_string([[ repeat foo() until nil]])) end) it("linearizes nested loops and breaks correctly", function() assert.equal([[ 1: Local ... (2..24) 2: Noop 3: Eval Call 4: Cjump -> 25 5: Eval Call 6: Noop 7: Eval Call 8: Cjump -> 18 9: Eval Call 10: Noop 11: Eval Call 12: Cjump -> 15 13: Jump -> 18 14: Jump -> 15 15: Eval Call 16: Noop 17: Jump -> 7 18: Noop 19: Eval Call 20: Cjump -> 23 21: Jump -> 25 22: Jump -> 23 23: Noop 24: Jump -> 3]], get_line_as_string([[ while cond() do stmts() while cond() do stmts() if cond() then break end stmts() end if cond() then break end end]])) end) it("linearizes if correctly", function() assert.equal([[ 1: Local ... (2..15) 2: Noop 3: Eval Call 4: Cjump -> 16 5: Noop 6: Eval Call 7: Cjump -> 10 8: Eval Call 9: Jump -> 15 10: Eval Call 11: Cjump -> 14 12: Eval Call 13: Jump -> 15 14: Eval Call 15: Jump -> 16]], get_line_as_string([[ if cond() then if cond() then stmts() elseif cond() then stmts() else stmts() end end]])) end) it("linearizes if with literal condition correctly", function() assert.equal([[ 1: Local ... (2..14) 2: Noop 3: Eval True 4: Noop 5: Eval Call 6: Cjump -> 9 7: Eval Call 8: Jump -> 14 9: Eval False 10: Jump -> 13 11: Eval Call 12: Jump -> 14 13: Eval Call 14: Jump -> 15]], get_line_as_string([[ if true then if cond() then stmts() elseif false then stmts() else stmts() end end]])) end) it("linearizes gotos correctly", function() assert.equal([[ 1: Local ... (2..13) 2: Eval Call 3: Noop 4: Jump -> 2 5: Eval Call 6: Noop 7: Jump -> 9 8: Eval Call 9: Eval Call 10: Noop 11: Noop 12: Jump -> 14 13: Eval Call]], get_line_as_string([[ ::label1:: stmts() goto label1 stmts() goto label2 stmts() ::label2:: stmts() do goto label2 stmts() ::label2:: end]])) end) end) describe("when registering values", function() it("registers values in empty chunk correctly", function() assert.equal([[ Local: ... (arg / arg, initial)]], get_value_info_as_string("")) end) it("registers values in assignments correctly", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial) Set: a (var / var)]], get_value_info_as_string([[ local a = b a = d]])) end) it("registers empty values correctly", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial), b (var / var, empty) Set: a (var / var), b (var / var)]], get_value_info_as_string([[ local a, b = 4 a, b = 5]])) end) it("registers function values as of type func", function() assert.equal([[ Local: ... (arg / arg, initial) Local: f (var / func, initial)]], get_value_info_as_string([[ local function f() end]])) end) it("registers overwritten args and counters as of type var", function() assert.equal([[ Local: ... (arg / arg, initial) Local: i (loopi / loopi, initial) Set: i (loopi / var)]], get_value_info_as_string([[ for i = 1, 10 do i = 6 end]])) end) it("registers groups of secondary values", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, initial), b (var / var, initial, 2 secondaries), c (var / var, initial, 2 secondaries) Set: a (var / var), b (var / var, 2 secondaries), c (var / var, 2 secondaries)]], get_value_info_as_string([[ local a, b, c = f(), g() a, b, c = f(), g()]])) end) it("marks groups of secondary values used if one of values is put into global or index", function() assert.equal([[ Local: ... (arg / arg, initial) Local: a (var / var, empty) Set: a (var / var, 1 secondaries, used)]], get_value_info_as_string([[ local a g, a = f()]])) end) end) end) luacheck-0.21.1/spec/luacheck_spec.lua000066400000000000000000000354251315524664100176200ustar00rootroot00000000000000local luacheck = require "luacheck" local function strip_locations(report) for _, file_report in ipairs(report) do for _, event in ipairs(file_report) do event.line = nil event.column = nil event.end_column = nil event.prev_line = nil event.prev_column = nil end end return report end describe("luacheck", function() it("is an alias of luacheck.check_files", function() assert.same(luacheck.check_files({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }), luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" })) end) it("panics on bad files", function() assert.has_error(function() luacheck("foo") end, "bad argument #1 to 'luacheck.check_files' (table expected, got string)") assert.has_error(function() luacheck({123}) end, "bad argument #1 to 'luacheck.check_files' (array of paths or file handles expected, got number)") end) it("panics on bad options", function() assert.has_error(function() luacheck({"foo"}, "bar") end, "bad argument #2 to 'luacheck.check_files' (table or nil expected, got string)") assert.has_error(function() luacheck({"foo"}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.check_files' (invalid value of option 'globals')") assert.has_error(function() luacheck({"foo"}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.check_files' (invalid value of option 'unused')") end) it("works on empty list", function() assert.same({ warnings = 0, errors = 0, fatals = 0 }, strip_locations(luacheck({}))) end) it("works on files", function() assert.same({ {}, { { code = "211", name = "helper", func = true }, { code = "212", name = "..." }, { code = "111", name = "embrace", indexing = {"embrace"}, top = true }, { code = "412", name = "opt" }, { code = "113", name = "hepler", indexing = {"hepler"} } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 5, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }))) end) it("uses options", function() assert.same({ {}, { { code = "111", name = "embrace", indexing = {"embrace"}, top = true }, { code = "412", name = "opt" }, { code = "113", name = "hepler", indexing = {"hepler"} } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 3, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }, { unused = false }))) end) it("uses option overrides", function() assert.same({ {}, { { code = "111", name = "embrace", indexing = {"embrace"}, top = true }, { code = "113", name = "hepler", indexing = {"hepler"} } }, { { code = "011", msg = "expected '=' near '__future__'" } }, warnings = 2, errors = 1, fatals = 0 }, strip_locations(luacheck({ "spec/samples/good_code.lua", "spec/samples/bad_code.lua", "spec/samples/python_code.lua" }, { nil, { global = true, unused = false, redefined = false }, global = false }))) end) end) describe("check_strings", function() it("panics on bad strings", function() assert.has_error(function() luacheck.check_strings("foo") end, "bad argument #1 to 'luacheck.check_strings' (table expected, got string)") assert.has_error(function() luacheck.check_strings({1}) end, "bad argument #1 to 'luacheck.check_strings' (array of strings or tables expected, got number)") end) it("panics on bad options", function() assert.has_error(function() luacheck.check_strings({"foo"}, "bar") end, "bad argument #2 to 'luacheck.check_strings' (table or nil expected, got string)") assert.has_error(function() luacheck.check_strings({"foo"}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.check_strings' (invalid value of option 'globals')") assert.has_error(function() luacheck.check_strings({"foo"}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.check_strings' (invalid value of option 'unused')") end) it("works on empty list", function() assert.same({ warnings = 0, errors = 0, fatals = 0 }, luacheck.check_strings({})) end) it("works on strings", function() assert.same({ { { code = "113", name = "foo", indexing = {"foo"} } }, { { code = "011", msg = "expected expression near 'return'" } }, warnings = 1, errors = 1, fatals = 0 }, strip_locations(luacheck.check_strings({"return foo", "return return"}))) end) it("supports comments in inline options", function() assert.same({ { { code = "211", name = "bar" } }, warnings = 1, errors = 0, fatals = 0 }, strip_locations(luacheck.check_strings({"local foo, bar -- luacheck: ignore foo (not bar though)"}))) end) it("provides correct location info for warnings", function() assert.same({ { { code = "521", label = "foo", line = 1, column = 1, end_column = 6 }, { code = "312", name = "self", line = 3, column = 11, end_column = 11, overwritten_line = 4, overwritten_column = 4 }, { code = "311", name = "self", line = 4, column = 4, end_column = 7, overwritten_line = 5, overwritten_column = 4 }, { code = "511", line = 9, column = 1, end_column = 1 } }, warnings = 4, errors = 0, fatals = 0 }, luacheck.check_strings({[[ :: foo ::local t = {} function t:m(x) self = x self = x return self end do return t end (t)() ]]})) end) it("provides correct location info for bad inline options", function() assert.same({ { { code = "022", line = 1, column = 1, end_column = 17 }, { code = "023", line = 3, column = 4, end_column = 26 }, { code = "021", msg = "unknown inline option 'some invalid comment'", line = 6, column = 10, end_column = 14 } }, warnings = 0, errors = 3, fatals = 0 }, luacheck.check_strings({[[ -- luacheck: push local function f() --[=[ luacheck: pop ]=] end return f --[=[ luacheck: some invalid comment ]=] ]]})) end) it("provides correct location info for syntax errors", function() assert.same({ { { code = "011", msg = "unfinished string", line = 1, column = 11, end_column = 11 } }, { { code = "011", msg = "invalid hexadecimal escape sequence '\\x2'", line = 1, column = 15, end_column = 17 } }, { { code = "011", msg = "expected 'then' near ", line = 1, column = 9, end_column = 9 } }, { { code = "011", msg = "label 'b' already defined on line 1", line = 1, column = 7, end_column = 11 } }, { { code = "011", msg = "cannot use '...' outside a vararg function", line = 1, column = 15, end_column = 17 } }, { { code = "011", msg = "'break' is not inside a loop", line = 1, column = 1, end_column = 5 } }, warnings = 0, errors = 6, fatals = 0 }, luacheck.check_strings({ [[local x = "foo]], [[local x = "foo\x2]], [[if true ]], [[::b:: ::b::]], [[function f() (...)() end]], [[break it()]] })) end) it("uses options", function() assert.same({ {}, { { code = "011", msg = "expected expression near 'return'" } }, warnings = 0, errors = 1, fatals = 0 }, strip_locations(luacheck.check_strings({"return foo", "return return"}, {ignore = {"113"}}))) end) it("ignores tables with .fatal field", function() assert.same({ { { code = "113", name = "foo", indexing = {"foo"} } }, { fatal = "I/O" }, warnings = 1, errors = 0, fatals = 1 }, strip_locations(luacheck.check_strings({"return foo", {fatal = "I/O"}}))) end) end) describe("get_report", function() it("panics on bad argument", function() assert.has_error(function() luacheck.get_report({}) end, "bad argument #1 to 'luacheck.get_report' (string expected, got table)") end) it("returns a table", function() assert.is_table(luacheck.get_report("return foo")) end) it("returns a table with single error event on syntax error", function() local report = strip_locations({luacheck.get_report("return return").events})[1] assert.same({code = "011", msg = "expected expression near 'return'"}, report[1]) end) end) describe("process_reports", function() it("panics on bad reports", function() assert.has_error(function() luacheck.process_reports("foo") end, "bad argument #1 to 'luacheck.process_reports' (table expected, got string)") end) it("panics on bad options", function() assert.has_error(function() luacheck.process_reports({{}}, "bar") end, "bad argument #2 to 'luacheck.process_reports' (table or nil expected, got string)") assert.has_error(function() luacheck.process_reports({{}}, {globals = "bar"}) end, "bad argument #2 to 'luacheck.process_reports' (invalid value of option 'globals')") assert.has_error(function() luacheck.process_reports({{}}, {{unused = 123}}) end, "bad argument #2 to 'luacheck.process_reports' (invalid value of option 'unused')") end) it("processes reports", function() assert.same({ { { code = "113", name = "foo", indexing = {"foo"} } }, {}, warnings = 1, errors = 0, fatals = 0 }, strip_locations(luacheck.process_reports( {luacheck.get_report("return foo"), luacheck.get_report("return math")}))) end) it("uses options", function() assert.same({ { { code = "113", name = "foo", indexing = {"foo"} } }, { { code = "113", name = "math", indexing = {"math", "floor"} } }, warnings = 2, errors = 0, fatals = 0 }, strip_locations(luacheck.process_reports( {luacheck.get_report("return foo"), luacheck.get_report("return math.floor")}, { std = "none" }))) end) end) describe("get_message", function() it("panics on bad events", function() assert.has_error(function() luacheck.get_message("foo") end, "bad argument #1 to 'luacheck.get_message' (table expected, got string)") end) it("returns message for an event", function() assert.equal("unused argument 'bar'", luacheck.get_message({ code = "212", name = "bar" })) assert.equal("shadowing definition of loop variable 'foo' on line 1", luacheck.get_message({ code = "423", name = "foo", line = 2, prev_line = 1 })) assert.equal("unused label 'fail'", luacheck.get_message({ code = "521", name = "unrelated", label = "fail" })) assert.equal("value assigned to field 'actual' is overwritten on line 2 before use", luacheck.get_message({ code = "314", name = "unrelated", field = "actual", overwritten_line = 2 })) assert.equal("value assigned to index '42' is overwritten on line 2 before use", luacheck.get_message({ code = "314", name = "11037", field = "42", index = true, overwritten_line = 2 })) assert.equal("message goes here", luacheck.get_message({ code = "011", msg = "message goes here" })) assert.equal("unexpected character near '%'", luacheck.get_message({ code = "011", msg = "unexpected character near '%'" })) end) end) luacheck-0.21.1/spec/options_spec.lua000066400000000000000000000140611315524664100175250ustar00rootroot00000000000000local options = require "luacheck.options" describe("options", function() describe("validate", function() it("returns true if options are empty", function() assert.is_true(options.validate(options.all_options)) end) it("returns true if options are valid", function() assert.is_true(options.validate(options.all_options, { globals = {"foo"}, compat = false, unrelated = function() end })) end) it("returns false if options are invalid", function() assert.is_false(options.validate(options.all_options, { globals = 3, redefined = false })) assert.is_false(options.validate(options.all_options, { globals = {3} })) assert.is_false(options.validate(options.all_options, function() end)) assert.is_false(options.validate(options.all_options, { unused = 0 })) end) it("additionally returns name of the problematic field", function() assert.equal("globals", select(2, options.validate(options.all_options, { globals = 3, redefined = false }))) assert.equal("globals", select(2, options.validate(options.all_options, { globals = {3} }))) assert.equal("unused", select(2, options.validate(options.all_options, { unused = 0 }))) end) end) describe("normalize", function() it("applies default values", function() local opts = options.normalize({}) assert.same(opts, options.normalize({{}})) assert.is_true(opts.unused_secondaries) assert.is_false(opts.module) assert.is_false(opts.allow_defined) assert.is_false(opts.allow_defined_top) assert.is_table(opts.std) assert.same({}, opts.rules) end) it("considers simple boolean options", function() local opts = options.normalize({ { module = false, unused_secondaries = true }, { module = true, allow_defined = false } }) assert.is_true(opts.module) assert.is_true(opts.unused_secondaries) assert.is_false(opts.allow_defined) end) it("considers opts.std and opts.compat", function() assert.same({fields = { baz = {read_only = false, other_fields = true} }}, options.normalize({ { std = "none" }, { globals = {"foo", "bar"}, compat = true }, { new_globals = {"baz"}, compat = false } }).std) end) it("allows compound std unions", function() assert.same(options.normalize({ { std = "max" }, }).globals, options.normalize({ { std = "lua51+lua52+lua53+luajit" }, }).globals) end) it("allows std addition", function() assert.same(options.normalize({ { std = "lua52 + busted" }, }).globals, options.normalize({ { std = "max" }, { std = "none" }, { std = "+lua52+busted" } }).globals) end) it("considers read-only and regular globals", function() local opts = options.normalize({ { std = "lua52", globals = {"foo", "bar", "removed"}, read_globals = {"baz"} }, { new_read_globals = {"quux"}, not_globals = {"removed", "unrelated", "print"} } }) local std = opts.std assert.is_table(std) assert.is_table(std.fields) assert.is_same({read_only = false, other_fields = true}, std.fields.foo) assert.is_same({read_only = false, other_fields = true}, std.fields.bar) assert.is_nil(std.fields.baz) assert.is_same({read_only = true, deep_read_only = true, other_fields = true}, std.fields.quux) assert.is_table(std.fields.string) assert.is_true(std.fields.string.deep_read_only) assert.is_nil(std.fields.string.other_fields) end) it("considers read-only and regular field definitions", function() local opts = options.normalize({ { std = "none", globals = {"foo", "bar.nested", "baz.nested.deeply"}, read_globals = {"bar", "foo.nested"} }, { not_globals = {"baz.nested", "unrelated.field"} } }) assert.same({ fields = { foo = { read_only = false, other_fields = true, fields = { nested = {deep_read_only = true, read_only = true, other_fields = true} } }, bar = { read_only = true, other_fields = true, fields = { nested = {read_only = false, other_fields = true} } }, baz = {deep_read_only = true, read_only = false, fields = {}} } }, opts.std) end) it("considers macros, ignore, enable and only", function() assert.same({ {{{nil, "^foo$"}}, "only"}, {{{"^21[23]", nil}}, "disable"}, {{{"^[23]", nil}}, "enable"}, {{{"^511", nil}}, "enable"}, {{{"^412", nil}, {"1$", "^bar$"}}, "disable"} }, options.normalize({ { unused = false }, { ignore = {"412", "1$/bar"} }, { unused = true, unused_args = false, enable = {"511"} }, { only = {"foo"} } }).rules) end) end) end) luacheck-0.21.1/spec/parser_spec.lua000066400000000000000000001504551315524664100173360ustar00rootroot00000000000000local parser = require "luacheck.parser" local function strip_locations(ast) ast.location = nil ast.end_location = nil ast.end_column = nil ast.equals_location = nil ast.first_token = nil for i=1, #ast do if type(ast[i]) == "table" then strip_locations(ast[i]) end end end local function get_ast(src) local ast = parser.parse(src) assert.is_table(ast) strip_locations(ast) return ast end local function get_node(src) return get_ast(src)[1] end local function get_expr(src) return get_node("return " .. src)[1] end local function get_comments(src) return (select(2, parser.parse(src))) end local function get_code_lines(src) return select(3, parser.parse(src)) end local function get_error(src) local ok, err = pcall(parser.parse, src) assert.is_false(ok) return err end describe("parser", function() it("parses empty source correctly", function() assert.same({}, get_ast(" ")) end) it("does not allow extra ending keywords", function() assert.same({line = 1, column = 1, end_column = 3, msg = "expected near 'end'"}, get_error("end")) end) it("parses return statement correctly", function() assert.same({tag = "Return"}, get_node("return")) assert.same({tag = "Return", {tag = "Number", "1"} }, get_node("return 1")) assert.same({tag = "Return", {tag = "Number", "1"}, {tag = "String", "foo"} }, get_node("return 1, 'foo'")) assert.same( {line = 1, column = 10, end_column = 10, msg = "expected expression near "}, get_error("return 1,") ) end) it("parses labels correctly", function() assert.same({tag = "Label", "fail"}, get_node("::fail::")) assert.same({tag = "Label", "fail"}, get_node("::\nfail\n::")) assert.same({line = 1, column = 3, end_column = 4, msg = "expected identifier near '::'"}, get_error("::::")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected identifier near '1'"}, get_error("::1::")) end) it("parses goto correctly", function() assert.same({tag = "Goto", "fail"}, get_node("goto fail")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected identifier near "}, get_error("goto")) assert.same( {line = 1, column = 9, end_column = 9, msg = "expected statement near ','"}, get_error("goto foo, bar") ) end) it("parses break correctly", function() assert.same({tag = "Break"}, get_node("break")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected '=' near "}, get_error("break fail")) end) it("parses do end correctly", function() assert.same({tag = "Do"}, get_node("do end")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected 'end' near "}, get_error("do")) assert.same( {line = 1, column = 4, end_column = 8, msg = "expected 'end' near 'until'"}, get_error("do until false") ) assert.same( {line = 2, column = 1, end_column = 5, msg = "expected 'end' (to close 'do' on line 1) near 'until'"}, get_error("do\nuntil false") ) end) it("parses while do end correctly", function() assert.same({tag = "While", {tag = "True"}, {} }, get_node("while true do end")) assert.same({line = 1, column = 6, end_column = 6, msg = "expected condition near "}, get_error("while")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected 'do' near "}, get_error("while true")) assert.same( {line = 1, column = 14, end_column = 14, msg = "expected 'end' near "}, get_error("while true do") ) assert.same( {line = 2, column = 3, end_column = 3, msg = "expected 'end' (to close 'while' on line 1) near "}, get_error("while true\ndo") ) assert.same( {line = 1, column = 7, end_column = 8, msg = "expected condition near 'do'"}, get_error("while do end") ) assert.same( {line = 1, column = 11, end_column = 11, msg = "expected 'do' near ','"}, get_error("while true, false do end") ) end) it("parses repeat until correctly", function() assert.same({tag = "Repeat", {}, {tag = "True"} }, get_node("repeat until true")) assert.same({line = 1, column = 7, end_column = 7, msg = "expected 'until' near "}, get_error("repeat")) assert.same( {line = 3, column = 1, end_column = 1, msg = "expected 'until' (to close 'repeat' on line 1) near "}, get_error("repeat\n--") ) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected condition near "}, get_error("repeat until") ) assert.same( {line = 1, column = 18, end_column = 18, msg = "expected statement near ','"}, get_error("repeat until true, false") ) end) describe("when parsing if", function() it("parses if then end correctly", function() assert.same({tag = "If", {tag = "True"}, {} }, get_node("if true then end")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected condition near "}, get_error("if")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected 'then' near "}, get_error("if true")) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected 'end' near "}, get_error("if true then") ) assert.same( {line = 2, column = 5, end_column = 5, msg = "expected 'end' (to close 'if' on line 1) near "}, get_error("if true\nthen") ) assert.same( {line = 1, column = 4, end_column = 7, msg = "expected condition near 'then'"}, get_error("if then end") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected 'then' near ','"}, get_error("if true, false then end") ) end) it("parses if then else end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {} }, get_node("if true then else end")) assert.same( {line = 1, column = 18, end_column = 18, msg = "expected 'end' near "}, get_error("if true then else") ) assert.same( {line = 3, column = 1, end_column = 1, msg = "expected 'end' (to close 'else' on line 2) near "}, get_error("if true\nthen else\n") ) assert.same( {line = 1, column = 19, end_column = 22, msg = "expected 'end' near 'else'"}, get_error("if true then else else end") ) end) it("parses if then elseif then end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {tag = "False"}, {} }, get_node("if true then elseif false then end")) assert.same( {line = 1, column = 21, end_column = 23, msg = "expected condition near 'end'"}, get_error("if true then elseif end") ) assert.same( {line = 1, column = 21, end_column = 24, msg = "expected condition near 'then'"}, get_error("if true then elseif then end") ) assert.same( {line = 2, column = 5, end_column = 5, msg = "expected 'end' (to close 'elseif' on line 1) near "}, get_error("if true then elseif a\nthen") ) end) it("parses if then elseif then else end correctly", function() assert.same({tag = "If", {tag = "True"}, {}, {tag = "False"}, {}, {} }, get_node("if true then elseif false then else end")) assert.same( {line = 1, column = 36, end_column = 36, msg = "expected 'end' near "}, get_error("if true then elseif false then else") ) end) end) describe("when parsing for", function() it("parses fornum correctly", function() assert.same({tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Op", "len", {tag = "Id", "t"}}, {} }, get_node("for i=1, #t do end")) assert.same( {line = 1, column = 4, end_column = 4, msg = "expected identifier near "}, get_error("for") ) assert.same( {line = 1, column = 6, end_column = 6, msg = "expected '=', ',' or 'in' near "}, get_error("for i") ) assert.same( {line = 1, column = 7, end_column = 8, msg = "expected '=', ',' or 'in' near '~='"}, get_error("for i ~= 2") ) assert.same( {line = 1, column = 11, end_column = 12, msg = "expected ',' near 'do'"}, get_error("for i = 2 do end") ) assert.same( {line = 1, column = 15, end_column = 15, msg = "expected 'end' near "}, get_error("for i=1, #t do") ) assert.same( {line = 2, column = 4, end_column = 4, msg = "expected 'end' (to close 'for' on line 1) near "}, get_error("for i=1, #t do\na()") ) assert.same( {line = 1, column = 5, end_column = 5, msg = "expected identifier near '('"}, get_error("for (i)=1, #t do end") ) assert.same( {line = 1, column = 5, end_column = 5, msg = "expected identifier near '3'"}, get_error("for 3=1, #t do end") ) end) it("parses fornum with step correctly", function() assert.same({tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Op", "len", {tag = "Id", "t"}}, {tag = "Number", "2"}, {} }, get_node("for i=1, #t, 2 do end")) assert.same( {line = 1, column = 15, end_column = 15, msg = "expected 'do' near ','"}, get_error("for i=1, #t, 2, 3 do") ) end) it("parses forin correctly", function() assert.same({tag = "Forin", { {tag = "Id", "i"} }, { {tag = "Id", "t"} }, {} }, get_node("for i in t do end")) assert.same({tag = "Forin", { {tag = "Id", "i"}, {tag = "Id", "j"} }, { {tag = "Id", "t"}, {tag = "String", "foo"} }, {} }, get_node("for i, j in t, 'foo' do end")) assert.same( {line = 1, column = 5, end_column = 6, msg = "expected identifier near 'in'"}, get_error("for in foo do end") ) assert.same( {line = 1, column = 10, end_column = 11, msg = "expected expression near 'do'"}, get_error("for i in do end") ) end) end) describe("when parsing functions", function() it("parses simple function correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {}, {}} } }, get_node("function a() end")) assert.same( {line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, get_error("function") ) assert.same( {line = 1, column = 11, end_column = 11, msg = "expected '(' near "}, get_error("function a") ) assert.same( {line = 1, column = 12, end_column = 12, msg = "expected argument near "}, get_error("function a(") ) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected 'end' near "}, get_error("function a()") ) assert.same( {line = 2, column = 2, end_column = 2, msg = "expected 'end' (to close 'function' on line 1) near "}, get_error("function a(\n)") ) assert.same( {line = 1, column = 10, end_column = 10, msg = "expected identifier near '('"}, get_error("function (a)()") ) assert.same( {line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function() end") ) assert.same( {line = 1, column = 11, end_column = 11, msg = "expected '(' near 'a'"}, get_error("(function a() end)") ) assert.same( {line = 1, column = 18, end_column = 18, msg = "expected expression near ')'"}, get_error("function a() end()") ) end) it("parses simple function with arguments correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}}, {}} } }, get_node("function a(b) end")) assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}, {tag = "Id", "c"}}, {}} } }, get_node("function a(b, c) end")) assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Function", {{tag = "Id", "b"}, {tag = "Dots", "..."}}, {}} } }, get_node("function a(b, ...) end")) assert.same( {line = 1, column = 15, end_column = 15, msg = "expected argument near ')'"}, get_error("function a(b, ) end") ) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected ')' near '.'"}, get_error("function a(b.c) end") ) assert.same( {line = 2, column = 2, end_column = 2, msg = "expected ')' (to close '(' on line 1) near '.'"}, get_error("function a(\nb.c) end") ) assert.same( {line = 1, column = 12, end_column = 12, msg = "expected argument near '('"}, get_error("function a((b)) end") ) assert.same( {line = 1, column = 15, end_column = 15, msg = "expected ')' near ','"}, get_error("function a(..., ...) end") ) end) it("parses field function correctly", function() assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Function", {}, {}} } }, get_node("function a.b() end")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Function", {}, {}} } }, get_node("function a.b.c() end")) assert.same( {line = 1, column = 11, end_column = 11, msg = "expected '(' near '['"}, get_error("function a[b]() end") ) assert.same( {line = 1, column = 12, end_column = 12, msg = "expected identifier near '('"}, get_error("function a.() end") ) end) it("parses method function correctly", function() assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Function", {{tag = "Id", "self", implicit = true}}, {}} } }, get_node("function a:b() end")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Function", {{tag = "Id", "self", implicit = true}}, {}} } }, get_node("function a.b:c() end")) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected '(' near '.'"}, get_error("function a:b.c() end") ) end) end) describe("when parsing local declarations", function() it("parses simple local declaration correctly", function() assert.same({tag = "Local", { {tag = "Id", "a"} } }, get_node("local a")) assert.same({tag = "Local", { {tag = "Id", "a"}, {tag = "Id", "b"} } }, get_node("local a, b")) assert.same( {line = 1, column = 6, end_column = 6, msg = "expected identifier near "}, get_error("local") ) assert.same( {line = 1, column = 9, end_column = 9, msg = "expected identifier near "}, get_error("local a,") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, get_error("local a.b") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, get_error("local a[b]") ) assert.same( {line = 1, column = 7, end_column = 7, msg = "expected identifier near '('"}, get_error("local (a)") ) end) it("parses local declaration with assignment correctly", function() assert.same({tag = "Local", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node("local a = b")) assert.same({tag = "Local", { {tag = "Id", "a"}, {tag = "Id", "b"} }, { {tag = "Id", "c"}, {tag = "Id", "d"} } }, get_node("local a, b = c, d")) assert.same( {line = 1, column = 11, end_column = 11, msg = "expected expression near "}, get_error("local a = ") ) assert.same( {line = 1, column = 13, end_column = 13, msg = "expected expression near "}, get_error("local a = b,") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected statement near '.'"}, get_error("local a.b = c") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected statement near '['"}, get_error("local a[b] = c") ) assert.same( {line = 1, column = 10, end_column = 10, msg = "expected identifier near '('"}, get_error("local a, (b) = c") ) end) it("parses local function declaration correctly", function() assert.same({tag = "Localrec", {tag = "Id", "a"}, {tag = "Function", {}, {}} }, get_node("local function a() end")) assert.same( {line = 1, column = 15, end_column = 15, msg = "expected identifier near "}, get_error("local function") ) assert.same( {line = 1, column = 17, end_column = 17, msg = "expected '(' near '.'"}, get_error("local function a.b() end") ) end) end) describe("when parsing assignments", function() it("parses single target assignment correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node("a = b")) assert.same({tag = "Set", { {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}} }, { {tag = "Id", "c"} } }, get_node("a.b = c")) assert.same({tag = "Set", { {tag = "Index", {tag = "Index", {tag = "Id", "a"}, {tag = "String", "b"}}, {tag = "String", "c"} } }, { {tag = "Id", "d"} } }, get_node("a.b.c = d")) assert.same({tag = "Set", { {tag = "Index", {tag = "Invoke", {tag = "Call", {tag = "Id", "f"}}, {tag = "String", "g"} }, {tag = "Number", "9"} } }, { {tag = "Id", "d"} } }, get_node("(f():g())[9] = d")) assert.same({line = 1, column = 2, end_column = 2, msg = "expected '=' near "}, get_error("a")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected expression near "}, get_error("a = ")) assert.same({line = 1, column = 5, end_column = 5, msg = "expected statement near '='"}, get_error("a() = b")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '('"}, get_error("(a) = b")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '1'"}, get_error("1 = b")) end) it("parses multi assignment correctly", function() assert.same({tag = "Set", { {tag = "Id", "a"}, {tag = "Id", "b"} }, { {tag = "Id", "c"}, {tag = "Id", "d"} } }, get_node("a, b = c, d")) assert.same( {line = 1, column = 5, end_column = 5, msg = "expected '=' near "}, get_error("a, b") ) assert.same( {line = 1, column = 4, end_column = 4, msg = "expected identifier or field near '='"}, get_error("a, = b") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected expression near "}, get_error("a, b = ") ) assert.same( {line = 1, column = 10, end_column = 10, msg = "expected expression near "}, get_error("a, b = c,") ) assert.same( {line = 1, column = 8, end_column = 8, msg = "expected call or indexing near '='"}, get_error("a, b() = c") ) assert.same( {line = 1, column = 4, end_column = 4, msg = "expected identifier or field near '('"}, get_error("a, (b) = c") ) end) end) describe("when parsing expression statements", function() it("parses calls correctly", function() assert.same({tag = "Call", {tag = "Id", "a"} }, get_node("a()")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "String", "b"} }, get_node("a'b'")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Table"} }, get_node("a{}")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} }, get_node("a(b)")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_node("a(b, c)")) assert.same({tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} }, get_node("(a)(b)")) assert.same({tag = "Call", {tag = "Call", {tag = "Id", "a"}, {tag = "Id", "b"} } }, get_node("(a)(b)()")) assert.same({line = 1, column = 2, end_column = 2, msg = "expected expression near ')'"}, get_error("()()")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected expression near "}, get_error("a(")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected ')' near "}, get_error("a(b")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected ')' (to close '(' on line 1) near "}, get_error("a(\nb")) assert.same({line = 2, column = 1, end_column = 2, 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''"}, get_error("'foo'()")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function() end ()")) end) it("parses method calls correctly", function() assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"} }, get_node("a:b()")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "String", "c"} }, get_node("a:b'c'")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Table"} }, get_node("a:b{}")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"} }, get_node("a:b(c)")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"}, {tag = "Id", "d"} }, get_node("a:b(c, d)")) assert.same({tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"}, {tag = "Id", "c"} }, get_node("(a):b(c)")) assert.same({tag = "Invoke", {tag = "Invoke", {tag = "Id", "a"}, {tag = "String", "b"} }, {tag = "String", "c"} }, get_node("a:b():c()")) assert.same({line = 1, column = 1, end_column = 1, msg = "expected statement near '1'"}, get_error("1:b()")) assert.same({line = 1, column = 1, end_column = 2, msg = "expected statement near ''''"}, get_error("'':a()")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected identifier near '('"}, get_error("function()end:b()")) assert.same({line = 1, column = 4, end_column = 4, msg = "expected method arguments near ':'"}, get_error("a:b:c()")) assert.same({line = 1, column = 3, end_column = 3, msg = "expected identifier near "}, get_error("a:")) end) end) describe("when parsing expressions", function() it("parses singleton expressions correctly", function() assert.same({tag = "Nil"}, get_expr("nil")) assert.same({tag = "True"}, get_expr("true")) assert.same({tag = "False"}, get_expr("false")) assert.same({tag = "Number", "1"}, get_expr("1")) assert.same({tag = "String", "1"}, get_expr("'1'")) assert.same({tag = "Table"}, get_expr("{}")) assert.same({tag = "Function", {}, {}}, get_expr("function() end")) assert.same({tag = "Dots", "..."}, get_expr("...")) end) it("parses table constructors correctly", function() assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a, b, c}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Pair", {tag = "String", "b"}, {tag = "Id", "c"}}, {tag = "Id", "d"} }, get_expr("{a, b = c, d}")) assert.same({tag = "Table", {tag = "String", "a"}, {tag = "Pair", {tag = "Id", "b"}, {tag = "Id", "c"}}, {tag = "Id", "d"} }, get_expr("{[[a]], [b] = c, d}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c,}")) assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Id", "b"}, {tag = "Id", "c"} }, get_expr("{a; b, c;}")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near ';'"}, get_error("return {;}")) assert.same({line = 1, column = 9, end_column = 9, msg = "expected expression near "}, get_error("return {")) assert.same({line = 1, column = 11, end_column = 13, msg = "expected '}' near 'end'"}, get_error("return {a end")) assert.same({line = 2, column = 1, end_column = 3, msg = "expected '}' (to close '{' on line 1) near 'end'"}, get_error("return {a\nend")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected ']' near "}, get_error("return {[a")) assert.same({line = 2, column = 2, end_column = 2, msg = "expected ']' (to close '[' on line 1) near "}, get_error("return {[\na")) assert.same({line = 1, column = 11, end_column = 11, msg = "expected expression near ','"}, get_error("return {a,,}")) assert.same({line = 1, column = 13, end_column = 13, msg = "expected expression near "}, get_error("return {a = ")) end) it("wraps last element in table constructors in parens when needed", function() assert.same({tag = "Table", {tag = "Id", "a"}, {tag = "Paren", {tag = "Call", {tag = "Id", "f"} } } }, get_expr("{a, (f())}")) assert.same({tag = "Table", {tag = "Call", {tag = "Id", "f"} }, {tag = "Id", "a"} }, get_expr("{(f()), a}")) assert.same({tag = "Table", {tag = "Pair", {tag = "String", "a"}, {tag = "Call", {tag = "Id", "f"} } } }, get_expr("{a = (f())}")) assert.same({tag = "Table", {tag = "Call", {tag = "Id", "f"} }, {tag = "Pair", {tag = "String", "a"}, {tag = "Id", "b"} } }, get_expr("{(f()), a = b}")) end) it("parses simple expressions correctly", function() assert.same({tag = "Op", "unm", {tag = "Number", "1"} }, get_expr("-1")) assert.same({tag = "Op", "add", {tag = "Op", "add", {tag = "Number", "1"}, {tag = "Number", "2"} }, {tag = "Number", "3"} }, get_expr("1+2+3")) assert.same({tag = "Op", "pow", {tag = "Number", "1"}, {tag = "Op", "pow", {tag = "Number", "2"}, {tag = "Number", "3"} } }, get_expr("1^2^3")) assert.same({tag = "Op", "concat", {tag = "String", "1"}, {tag = "Op", "concat", {tag = "String", "2"}, {tag = "String", "3"} } }, get_expr("'1'..'2'..'3'")) end) it("handles operator precedence correctly", function() assert.same({tag = "Op", "add", {tag = "Op", "unm", {tag = "Number", "1"} }, {tag = "Op", "mul", {tag = "Number", "2"}, {tag = "Op", "pow", {tag = "Number", "3"}, {tag = "Number", "4"} } } }, get_expr("-1+2*3^4")) assert.same({tag = "Op", "bor", {tag = "Op", "bor", {tag = "Op", "band", {tag = "Op", "shr", {tag = "Number", "1"}, {tag = "Number", "2"} }, {tag = "Op", "shl", {tag = "Number", "3"}, {tag = "Number", "4"} } }, {tag = "Op", "bxor", {tag = "Number", "5"}, {tag = "Number", "6"} } }, {tag = "Op", "bnot", {tag = "Number", "7"} } }, get_expr("1 >> 2 & 3 << 4 | 5 ~ 6 | ~7")) assert.same({tag = "Op", "or", {tag = "Op", "and", {tag = "Op", "eq", {tag = "Id", "a"}, {tag = "Id", "b"} }, {tag = "Op", "eq", {tag = "Id", "c"}, {tag = "Id", "d"} } }, {tag = "Op", "ne", {tag = "Id", "e"}, {tag = "Id", "f"} } }, get_expr("a == b and c == d or e ~= f")) end) it("wraps last expression in a list in parens when needed", function() assert.same({tag = "Return", {tag = "Dots", "..."}, {tag = "Paren", {tag = "Dots", "..."}} }, get_node("return (...), (...)")) assert.same({tag = "Return", {tag = "Dots", "..."}, {tag = "Dots", "..."} }, get_node("return (...), ...")) assert.same({tag = "Return", {tag = "True"}, {tag = "False"} }, get_node("return (true), (false)")) assert.same({tag = "Return", {tag = "Call", {tag = "Id", "f"} }, {tag = "Paren", {tag = "Call", {tag = "Id", "g"} } } }, get_node("return (f()), (g())")) assert.same({tag = "Return", {tag = "Invoke", {tag = "Id", "f"}, {tag = "String", "n"} }, {tag = "Paren", {tag = "Invoke", {tag = "Id", "g"}, {tag = "String", "m"} } } }, get_node("return (f:n()), (g:m())")) end) end) describe("when parsing multiple statements", function() it("considers semicolons and comments no-op statements", function() assert.same({tag = "Set", { {tag = "Id", "a"} }, { {tag = "Id", "b"} } }, get_node(";;;a = b;--[[]];--;")) end) it("does not allow statements after return", function() assert.same({line = 1, column = 8, end_column = 12, msg = "expected expression near 'break'"}, get_error("return break")) assert.same({line = 1, column = 9, end_column = 13, msg = "expected end of block near 'break'"}, get_error("return; break")) assert.same({line = 1, column = 8, end_column = 8, msg = "expected end of block near ';'"}, get_error("return;;")) assert.same({line = 1, column = 10, end_column = 14, msg = "expected end of block near 'break'"}, get_error("return 1 break")) assert.same({line = 1, column = 11, end_column = 15, msg = "expected end of block near 'break'"}, get_error("return 1; break")) assert.same({line = 1, column = 13, end_column = 17, msg = "expected end of block near 'break'"}, get_error("return 1, 2 break")) assert.same({line = 1, column = 14, end_column = 18, msg = "expected end of block near 'break'"}, get_error("return 1, 2; break")) end) it("parses nested statements correctly", function() assert.same({ {tag = "Localrec", {tag = "Id", "f"}, {tag = "Function", {}, { {tag = "While", {tag = "True"}, { {tag = "If", {tag = "Nil"}, { {tag = "Call", {tag = "Id", "f"} }, {tag = "Return"} }, {tag = "False"}, { {tag = "Call", {tag = "Id", "g"} }, {tag = "Break"} }, { {tag = "Call", {tag = "Id", "h"} }, {tag = "Repeat", { {tag = "Goto", "fail"} }, {tag = "Id", "get_forked"} } } } } }, {tag = "Label", "fail"} }} }, {tag = "Do", {tag = "Fornum", {tag = "Id", "i"}, {tag = "Number", "1"}, {tag = "Number", "2"}, { {tag = "Call", {tag = "Id", "nothing"} } } }, {tag = "Forin", { {tag = "Id", "k"}, {tag = "Id", "v"} }, { {tag = "Call", {tag = "Id", "pairs"} } }, { {tag = "Call", {tag = "Id", "print"}, {tag = "String", "bar"} }, {tag = "Call", {tag = "Id", "assert"}, {tag = "Number", "42"} } } }, {tag = "Return"} }, }, get_ast([[ local function f() while true do if nil then f() return elseif false then g() break else h() repeat goto fail until get_forked end end ::fail:: end do for i=1, 2 do nothing() end for k, v in pairs() do print("bar") assert(42) end return end ]])) end) end) it("provides correct location info", function() assert.same({ {tag = "Localrec", location = {line = 1, column = 1, offset = 1}, first_token = "local", {tag = "Id", "foo", location = {line = 1, column = 16, offset = 16}}, {tag = "Function", location = {line = 1, column = 7, offset = 7}, end_location = {line = 4, column = 1, offset = 78}, { {tag = "Id", "a", location = {line = 1, column = 20, offset = 20}}, {tag = "Id", "b", location = {line = 1, column = 23, offset = 23}}, {tag = "Id", "c", location = {line = 1, column = 26, offset = 26}}, {tag = "Dots", "...", location = {line = 1, column = 29, offset = 29}} }, { {tag = "Local", location = {line = 2, column = 4, offset = 37}, first_token = "local", equals_location = {line = 2, column = 12, offset = 45}, { {tag = "Id", "d", location = {line = 2, column = 10, offset = 43}} }, { {tag = "Op", "mul", location = {line = 2, column = 15, offset = 48}, {tag = "Op", "add", location = {line = 2, column = 15, offset = 48}, {tag = "Id", "a", location = {line = 2, column = 15, offset = 48}}, {tag = "Id", "b", location = {line = 2, column = 19, offset = 52}} }, {tag = "Id", "c", location = {line = 2, column = 24, offset = 57}} } } }, {tag = "Return", location = {line = 3, column = 4, offset = 62}, first_token = "return", {tag = "Id", "d", location = {line = 3, column = 11, offset = 69}}, {tag = "Paren", location = {line = 3, column = 15, offset = 73}, {tag = "Dots", "...", location = {line = 3, column = 15, offset = 73}} } } } } }, {tag = "Set", location = {line = 6, column = 1, offset = 83}, first_token = "function", { {tag = "Index", location = {line = 6, column = 10, offset = 92}, {tag = "Id", "t", location = {line = 6, column = 10, offset = 92}}, {tag = "String", "bar", location = {line = 6, column = 12, offset = 94}} } }, { {tag = "Function", location = {line = 6, column = 1, offset = 83}, end_location = {line = 10, column = 1, offset = 142}, { {tag = "Id", "self", implicit = true, location = {line = 6, column = 11, offset = 93}}, {tag = "Id", "arg", location = {line = 6, column = 16, offset = 98}} }, { {tag = "If", location = {line = 7, column = 4, offset = 106}, first_token = "if", {tag = "Id", "arg", location = {line = 7, column = 7, offset = 109}, first_token = "arg"}, {location = {line = 7, column = 11, offset = 113}, -- Branch location. {tag = "Call", location = {line = 8, column = 7, offset = 124}, first_token = "print", {tag = "Id", "print", location = {line = 8, column = 7, offset = 124}}, {tag = "Id", "arg", location = {line = 8, column = 13, offset = 130}} } } } } } } } }, (parser.parse([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) end function t:bar(arg) if arg then print(arg) end end ]]))) end) it("provides correct location info for labels", function() assert.same({ {tag = "Label", "foo", location = {line = 1, column = 1, offset = 1}, end_column = 7, first_token = "::"}, {tag = "Label", "bar", location = {line = 2, column = 1, offset = 9}, end_column = 6, first_token = "::"}, {tag = "Label", "baz", location = {line = 3, column = 3, offset = 18}, end_column = 4, first_token = "::"} }, (parser.parse([[ ::foo:: :: bar :::: baz:: ]]))) end) it("provides correct location info for statements starting with expressions", function() assert.same({ {tag = "Call", location = {line = 1, column = 1, offset = 1}, first_token = "a", {tag = "Id", "a", location = {line = 1, column = 1, offset = 1}} }, {tag = "Call", location = {line = 2, column = 1, offset = 6}, first_token = "(", {tag = "Id", "b", location = {line = 2, column = 2, offset = 7}} }, {tag = "Set", location = {line = 3, column = 1, offset = 13}, first_token = "(", equals_location = {line = 3, column = 12, offset = 24}, { {tag = "Index", location = {line = 3, column = 3, offset = 15}, {tag = "Index", location = {line = 3, column = 3, offset = 15}, {tag = "Id", "c", location = {line = 3, column = 3, offset = 15}}, {tag = "String", "d", location = {line = 3, column = 6, offset = 18}} }, {tag = "Number", "3", location = {line = 3, column = 9, offset = 21}} } }, { {tag = "Number", "2", location = {line = 3, column = 14, offset = 26}} } } }, (parser.parse([[ a(); (b)(); ((c).d)[3] = 2 ]]))) end) 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([[ 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([[ 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([[ local function foo(a, b, c, ...) local d = (a + b) * c return d, (...) end function t:bar(arg) if arg then printarg) end end ]])) end) describe("providing misc information", function() it("provides comments correctly", function() assert.same({ {contents = " ignore something", location = {line = 1, column = 1, offset = 1}, end_column = 19}, {contents = " comments", location = {line = 2, column = 13, offset = 33}, end_column = 23}, {contents = "long comment", location = {line = 3, column = 13, offset = 57}, end_column = 17} }, get_comments([[ -- ignore something foo = bar() -- comments return true --[=[ long comment]=] ]])) end) it("provides lines with code correctly", function() assert.same({nil, true, true, true, true, true, true, true, true, nil, nil, true, true}, get_code_lines([[ -- nothing here local foo = 2 + 3 + [=[ ]=] + { --[=[empty]=] } ::bar:: ]])) end) end) end) luacheck-0.21.1/spec/samples/000077500000000000000000000000001315524664100157575ustar00rootroot00000000000000luacheck-0.21.1/spec/samples/argparse.lua000066400000000000000000000575011315524664100202760ustar00rootroot00000000000000local Parser, Command, Argument, Option do -- Create classes with setters local class = require "30log" local function add_setters(cl, fields) for field, setter in pairs(fields) do cl[field] = function(self, value) setter(self, value) self["_"..field] = value return self end end cl.__init = function(self, ...) return self(...) end cl.__call = function(self, ...) local name_or_options for i=1, select("#", ...) do name_or_options = select(i, ...) if type(name_or_options) == "string" then if self._aliases then table.insert(self._aliases, name_or_options) end if not self._aliases or not self._name then self._name = name_or_options end elseif type(name_or_options) == "table" then for field, setter in pairs(fields) do if name_or_options[field] ~= nil then self[field](self, name_or_options[field]) end end end end return self end return cl end local typecheck = setmetatable({}, { __index = function(self, type_) local typechecker_factory = function(field) return function(_, value) if type(value) ~= type_ then error(("bad field '%s' (%s expected, got %s)"):format(field, type_, type(value))) end end end self[type_] = typechecker_factory return typechecker_factory end }) local function aliased_name(self, name) typecheck.string "name" (self, name) table.insert(self._aliases, name) end local function aliased_aliases(self, aliases) typecheck.table "aliases" (self, aliases) if not self._name then self._name = aliases[1] end end local function parse_boundaries(boundaries) if tonumber(boundaries) then return tonumber(boundaries), tonumber(boundaries) end if boundaries == "*" then return 0, math.huge end if boundaries == "+" then return 1, math.huge end if boundaries == "?" then return 0, 1 end if boundaries:match "^%d+%-%d+$" then local min, max = boundaries:match "^(%d+)%-(%d+)$" return tonumber(min), tonumber(max) end if boundaries:match "^%d+%+$" then local min = boundaries:match "^(%d+)%+$" return tonumber(min), math.huge end end local function boundaries(field) return function(self, value) local min, max = parse_boundaries(value) if not min then error(("bad field '%s'"):format(field)) end self["_min"..field], self["_max"..field] = min, max end end local function convert(self, value) if type(value) ~= "function" then if type(value) ~= "table" then error(("bad field 'convert' (function or table expected, got %s)"):format(type(value))) end end end local function argname(self, value) if type(value) ~= "string" then if type(value) ~= "table" then error(("bad field 'argname' (string or table expected, got %s)"):format(type(value))) end end end local function add_help(self, param) if self._has_help then table.remove(self._options) self._has_help = false end if param then local help = self:flag() :description "Show this help message and exit. " :action(function() io.stdout:write(self:get_help() .. "\r\n") os.exit(0) end)(param) if not help._name then help "-h" "--help" end self._has_help = true end end Parser = add_setters(class { __name = "Parser", _arguments = {}, _options = {}, _commands = {}, _mutexes = {}, _require_command = true }, { name = typecheck.string "name", description = typecheck.string "description", epilog = typecheck.string "epilog", require_command = typecheck.boolean "require_command", usage = typecheck.string "usage", help = typecheck.string "help", add_help = add_help }) Command = add_setters(Parser:extends { __name = "Command", _aliases = {} }, { name = aliased_name, aliases = aliased_aliases, description = typecheck.string "description", epilog = typecheck.string "epilog", target = typecheck.string "target", require_command = typecheck.boolean "require_command", action = typecheck["function"] "action", usage = typecheck.string "usage", help = typecheck.string "help", add_help = add_help }) Argument = add_setters(class { __name = "Argument", _minargs = 1, _maxargs = 1, _mincount = 1, _maxcount = 1, _defmode = "unused" }, { name = typecheck.string "name", description = typecheck.string "description", target = typecheck.string "target", args = boundaries "args", default = typecheck.string "default", defmode = typecheck.string "defmode", convert = convert, argname = argname }) Option = add_setters(Argument:extends { __name = "Option", _aliases = {}, _mincount = 0, _overwrite = true }, { name = aliased_name, aliases = aliased_aliases, description = typecheck.string "description", target = typecheck.string "target", args = boundaries "args", count = boundaries "count", default = typecheck.string "default", defmode = typecheck.string "defmode", convert = convert, overwrite = typecheck.boolean "overwrite", action = typecheck["function"] "action", argname = argname }) end function Argument:_get_argument_list() local buf = {} local i = 1 while i <= math.min(self._minargs, 3) do local argname = self:_get_argname_i(i) if self._default and self._defmode:find "a" then argname = "[" .. argname .. "]" end table.insert(buf, argname) i = i+1 end while i <= math.min(self._maxargs, 3) do table.insert(buf, "[" .. self:_get_argname_i(i) .. "]") i = i+1 if self._maxargs == math.huge then break end end if i < self._maxargs then table.insert(buf, "...") end return buf end function Argument:_get_usage() local usage = table.concat(self:_get_argument_list(), " ") if self._default and self._defmode:find "u" then if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then usage = "[" .. usage .. "]" end end return usage end function Argument:_get_type() if self._maxcount == 1 then if self._maxargs == 0 then return "flag" elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then return "arg" else return "multiarg" end else if self._maxargs == 0 then return "counter" elseif self._maxargs == 1 and self._minargs == 1 then return "multicount" else return "twodimensional" end end end function Argument:_get_argname_i(i) local argname = self:_get_argname() if type(argname) == "table" then return argname[i] else return argname end end function Argument:_get_argname() return self._argname or ("<"..self._name..">") end function Option:_get_argname() return self._argname or ("<"..self:_get_target()..">") end function Argument:_get_label() return self._name end function Option:_get_label() local variants = {} local argument_list = self:_get_argument_list() table.insert(argument_list, 1, nil) for _, alias in ipairs(self._aliases) do argument_list[1] = alias table.insert(variants, table.concat(argument_list, " ")) end return table.concat(variants, ", ") end function Command:_get_label() return table.concat(self._aliases, ", ") end function Argument:_get_description() if self._default then if self._description then return ("%s (default: %s)"):format(self._description, self._default) else return ("default: %s"):format(self._default) end else return self._description or "" end end function Command:_get_description() return self._description or "" end function Option:_get_usage() local usage = self:_get_argument_list() table.insert(usage, 1, self._name) usage = table.concat(usage, " ") if self._mincount == 0 or self._default then usage = "[" .. usage .. "]" end return usage end function Option:_get_target() if self._target then return self._target end for _, alias in ipairs(self._aliases) do if alias:sub(1, 1) == alias:sub(2, 2) then return alias:sub(3) end end return self._name:sub(2) end function Parser:_get_fullname() local parent = self._parent local buf = {self._name} while parent do table.insert(buf, 1, parent._name) parent = parent._parent end return table.concat(buf, " ") end function Parser:_update_charset(charset) charset = charset or {} for _, command in ipairs(self._commands) do command:_update_charset(charset) end for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do charset[alias:sub(1, 1)] = true end end return charset end function Parser:argument(...) local argument = Argument:new(...) table.insert(self._arguments, argument) return argument end function Parser:option(...) local option = Option:new(...) if self._has_help then table.insert(self._options, #self._options, option) else table.insert(self._options, option) end return option end function Parser:flag(...) return self:option():args(0)(...) end function Parser:command(...) local command = Command:new():add_help(true)(...) command._parent = self table.insert(self._commands, command) return command end function Parser:mutex(...) local options = {...} for i, option in ipairs(options) do assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i)) end table.insert(self._mutexes, options) return self end local max_usage_width = 70 local usage_welcome = "Usage: " function Parser:get_usage() if self._usage then return self._usage end local lines = {usage_welcome .. self:_get_fullname()} local function add(s) if #lines[#lines]+1+#s <= max_usage_width then lines[#lines] = lines[#lines] .. " " .. s else lines[#lines+1] = (" "):rep(#usage_welcome) .. s end end -- set of mentioned elements local used = {} for _, mutex in ipairs(self._mutexes) do local buf = {} for _, option in ipairs(mutex) do table.insert(buf, option:_get_usage()) used[option] = true end add("(" .. table.concat(buf, " | ") .. ")") end for _, elements in ipairs{self._options, self._arguments} do for _, element in ipairs(elements) do if not used[element] then add(element:_get_usage()) end end end if #self._commands > 0 then if self._require_command then add("") else add("[]") end add("...") end return table.concat(lines, "\r\n") end local margin_len = 3 local margin_len2 = 25 local margin = (" "):rep(margin_len) local margin2 = (" "):rep(margin_len2) local function make_two_columns(s1, s2) if s2 == "" then return margin .. s1 end s2 = s2:gsub("[\r\n][\r\n]?", function(sub) if #sub == 1 or sub == "\r\n" then return "\r\n" .. margin2 else return "\r\n\r\n" .. margin2 end end) if #s1 < (margin_len2-margin_len) then return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2 else return margin .. s1 .. "\r\n" .. margin2 .. s2 end end function Parser:get_help() if self._help then return self._help end local blocks = {self:get_usage()} if self._description then table.insert(blocks, self._description) end local labels = {"Arguments: ", "Options: ", "Commands: "} for i, elements in ipairs{self._arguments, self._options, self._commands} do if #elements > 0 then local buf = {labels[i]} for _, element in ipairs(elements) do table.insert(buf, make_two_columns(element:_get_label(), element:_get_description())) end table.insert(blocks, table.concat(buf, "\r\n")) end end if self._epilog then table.insert(blocks, self._epilog) end return table.concat(blocks, "\r\n\r\n") end local function get_tip(context, wrong_name) local context_pool = {} local possible_name local possible_names = {} for name in pairs(context) do for i=1, #name do possible_name = name:sub(1, i-1) .. name:sub(i+1) if not context_pool[possible_name] then context_pool[possible_name] = {} end table.insert(context_pool[possible_name], name) end end for i=1, #wrong_name+1 do possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1) if context[possible_name] then possible_names[possible_name] = true elseif context_pool[possible_name] then for _, name in ipairs(context_pool[possible_name]) do possible_names[name] = true end end end local first = next(possible_names) if first then if next(possible_names, first) then local possible_names_arr = {} for name in pairs(possible_names) do table.insert(possible_names_arr, "'" .. name .. "'") end table.sort(possible_names_arr) return "\r\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" else return "\r\nDid you mean '" .. first .. "'?" end else return "" end end local function plural(x) if x == 1 then return "" end return "s" end local default_cmdline = arg or {} function Parser:_parse(args, errhandler) args = args or default_cmdline local parser local charset local options = {} local arguments = {} local commands local option_mutexes = {} local used_mutexes = {} local opt_context = {} local com_context local result = {} local invocations = {} local passed = {} local cur_option local cur_arg_i = 1 local cur_arg local targets = {} local function error_(fmt, ...) return errhandler(parser, fmt:format(...)) end local function assert_(assertion, ...) return assertion or error_(...) end local function convert(element, data) if element._convert then local ok, err if type(element._convert) == "function" then ok, err = element._convert(data) else ok, err = element._convert[data] end assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'") data = ok end return data end local invoke, pass, close function invoke(element) local overwrite = false if invocations[element] == element._maxcount then if element._overwrite then overwrite = true else error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount)) end else invocations[element] = invocations[element]+1 end passed[element] = 0 local type_ = element:_get_type() local target = targets[element] if type_ == "flag" then result[target] = true elseif type_ == "multiarg" then result[target] = {} elseif type_ == "counter" then if not overwrite then result[target] = result[target]+1 end elseif type_ == "multicount" then if overwrite then table.remove(result[target], 1) end elseif type_ == "twodimensional" then table.insert(result[target], {}) if overwrite then table.remove(result[target], 1) end end if element._maxargs == 0 then close(element) end end function pass(element, data) passed[element] = passed[element]+1 data = convert(element, data) local type_ = element:_get_type() local target = targets[element] if type_ == "arg" then result[target] = data elseif type_ == "multiarg" or type_ == "multicount" then table.insert(result[target], data) elseif type_ == "twodimensional" then table.insert(result[target][#result[target]], data) end if passed[element] == element._maxargs then close(element) end end local function complete_invocation(element) while passed[element] < element._minargs do pass(element, element._default) end end function close(element) if passed[element] < element._minargs then if element._default and element._defmode:find "a" then complete_invocation(element) else error_("too few arguments") end else if element == cur_option then cur_option = nil elseif element == cur_arg then cur_arg_i = cur_arg_i+1 cur_arg = arguments[cur_arg_i] end end end local function switch(p) parser = p for _, option in ipairs(parser._options) do table.insert(options, option) for _, alias in ipairs(option._aliases) do opt_context[alias] = option end local type_ = option:_get_type() targets[option] = option:_get_target() if type_ == "counter" then result[targets[option]] = 0 elseif type_ == "multicount" or type_ == "twodimensional" then result[targets[option]] = {} end invocations[option] = 0 end for _, mutex in ipairs(parser._mutexes) do for _, option in ipairs(mutex) do if not option_mutexes[option] then option_mutexes[option] = {mutex} else table.insert(option_mutexes[option], mutex) end end end for _, argument in ipairs(parser._arguments) do table.insert(arguments, argument) invocations[argument] = 0 targets[argument] = argument._target or argument._name invoke(argument) end cur_arg = arguments[cur_arg_i] commands = parser._commands com_context = {} for _, command in ipairs(commands) do targets[command] = command._target or command._name for _, alias in ipairs(command._aliases) do com_context[alias] = command end end end local function get_option(name) return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name)) end local function do_action(element) if element._action then element._action() end end local function handle_argument(data) if cur_option then pass(cur_option, data) elseif cur_arg then pass(cur_arg, data) else local com = com_context[data] if not com then if #commands > 0 then error_("unknown command '%s'%s", data, get_tip(com_context, data)) else error_("too many arguments") end else result[targets[com]] = true do_action(com) switch(com) end end end local function handle_option(data) if cur_option then close(cur_option) end cur_option = opt_context[data] if option_mutexes[cur_option] then for _, mutex in ipairs(option_mutexes[cur_option]) do if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name) else used_mutexes[mutex] = cur_option end end end do_action(cur_option) invoke(cur_option) end local function mainloop() local handle_options = true for _, data in ipairs(args) do local plain = true local first, name, option if handle_options then first = data:sub(1, 1) if charset[first] then if #data > 1 then plain = false if data:sub(2, 2) == first then if #data == 2 then if cur_option then close(cur_option) end handle_options = false else local equal = data:find "=" if equal then name = data:sub(1, equal-1) option = get_option(name) assert_(option._maxargs > 0, "option '%s' does not take arguments", name) handle_option(data:sub(1, equal-1)) handle_argument(data:sub(equal+1)) else get_option(data) handle_option(data) end end else for i = 2, #data do name = first .. data:sub(i, i) option = get_option(name) handle_option(name) if i ~= #data and option._minargs > 0 then handle_argument(data:sub(i+1)) break end end end end end end if plain then handle_argument(data) end end end switch(self) charset = parser:_update_charset() mainloop() if cur_option then close(cur_option) end while cur_arg do if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then complete_invocation(cur_arg) else close(cur_arg) end end if parser._require_command and #commands > 0 then error_("a command is required") end for _, option in ipairs(options) do if invocations[option] == 0 then if option._default and option._defmode:find "u" then invoke(option) complete_invocation(option) close(option) end end if invocations[option] < option._mincount then if option._default and option._defmode:find "a" then while invocations[option] < option._mincount do invoke(option) close(option) end else error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount)) end end end return result end function Parser:error(msg) if _TEST then error(msg) else io.stderr:write(("%s\r\n\r\nError: %s\r\n"):format(self:get_usage(), msg)) os.exit(1) end end function Parser:parse(args) return self:_parse(args, Parser.error) end function Parser:pparse(args) local errmsg local ok, result = pcall(function() return self:_parse(args, function(parser, err) errmsg = err return error() end) end) if ok then return true, result else assert(errmsg, result) return false, errmsg end end return function(...) return Parser(default_cmdline[0]):add_help(true)(...) end luacheck-0.21.1/spec/samples/bad.rockspec000066400000000000000000000000121315524664100202310ustar00rootroot00000000000000build = 0 luacheck-0.21.1/spec/samples/bad_code.lua000066400000000000000000000002321315524664100201770ustar00rootroot00000000000000package.loaded[...] = {} local function helper(...) -- NYI end function embrace(opt) local opt = opt or "default" return hepler(opt.."?") end luacheck-0.21.1/spec/samples/bad_flow.lua000066400000000000000000000006741315524664100202460ustar00rootroot00000000000000if io.read("*n") == "exit" then else local a, b do -- print(something) end for _ = 1, 10 do if io.read("*n") == "foobar" then a, b = 1 print(a, b) break else a, b = 1, 2, 3 print(a, b) break end print("How could this happen?") end end while true do if package.loaded.foo then return 4 else print(5) break end end luacheck-0.21.1/spec/samples/bad_whitespace.lua000066400000000000000000000015211315524664100214230ustar00rootroot00000000000000-- Examples of whitespace formatting violations local function trailing_whitespace_in_code() return "This is awful" end local function trailing_whitespace_in_comment() -- Less awful, but... return "Still bad" end local function trailing_whitespace_in_long_strings() return [[ It gets worse Much worse ]] --[[ Same in long comments ]] end local function trailing_whitespace_mixed() return "Not much better" -- You bet! end local function whitespace_only_lines() return "Lost in space" end local function inconsistent_indentation() return "Don't do this" end return { -- fake "module" table trailing_whitespace_in_code, trailing_whitespace_in_comment, trailing_whitespace_in_long_strings, trailing_whitespace_mixed, whitespace_only_lines, inconsistent_indentation, } luacheck-0.21.1/spec/samples/compat.lua000066400000000000000000000000511315524664100177410ustar00rootroot00000000000000(setfenv and rawlen)(setfenv and rawlen) luacheck-0.21.1/spec/samples/custom_std_inline_options.lua000066400000000000000000000002211315524664100237520ustar00rootroot00000000000000-- luacheck: push -- luacheck: std +busted tostring(setfenv, print(it)) -- luacheck: pop -- luacheck: std other_std tostring(setfenv, print(it)) luacheck-0.21.1/spec/samples/defined.lua000066400000000000000000000000521315524664100200550ustar00rootroot00000000000000foo = {} function foo.bar() baz() end luacheck-0.21.1/spec/samples/defined2.lua000066400000000000000000000000121315524664100201330ustar00rootroot00000000000000foo.bar() luacheck-0.21.1/spec/samples/defined3.lua000066400000000000000000000000331315524664100201370ustar00rootroot00000000000000foo = {} foo = {} bar = {} luacheck-0.21.1/spec/samples/defined4.lua000066400000000000000000000000521315524664100201410ustar00rootroot00000000000000function foo() foo = 1 bar = {} end luacheck-0.21.1/spec/samples/empty.lua000066400000000000000000000000001315524664100176060ustar00rootroot00000000000000luacheck-0.21.1/spec/samples/global_fields.lua000066400000000000000000000013141315524664100212470ustar00rootroot00000000000000local t = table local upsert = t.upsert local update = upsert print(update.something) upsert = t.insert upsert() upsert.foo = "bar" package.loaded.hello = true package.loaded[package] = true local s1 = "find" local s2 = "gfind" -- luacheck: push std max print(string[s1]) print(string[s2]) -- luacheck: pop -- luacheck: push std min print(string[s1]) print(string[s2]) -- luacheck: pop -- luacheck: not globals string.find print(string[s1]) -- luacheck: globals nest.nest.nest nest.nest = "nest" print(server) print(server.sessions) server.foo = "bar" server.bar[_G] = "baz" server.baz = "abcd" print(server.baz.abcd) server.sessions["hey"] = "you" -- luacheck: std +my_server server.bar = 1 server.baz = 2 luacheck-0.21.1/spec/samples/global_inline_options.lua000066400000000000000000000004371315524664100230370ustar00rootroot00000000000000-- luacheck: allow defined top foo = 4 print(foo) bar = 6 -- luacheck: ignore 131 function f() baz = 5 -- luacheck: allow defined qu = 4 print(qu) end -- luacheck: module, globals external quu = 7 print(external) local function g() -- luacheck: ignore external = 8 end luacheck-0.21.1/spec/samples/globals.lua000066400000000000000000000000411315524664100201000ustar00rootroot00000000000000print(setfenv(rawlen(tostring))) luacheck-0.21.1/spec/samples/good_code.lua000066400000000000000000000002551315524664100204060ustar00rootroot00000000000000local embracer = {} local function helper() -- NYI wontfix end function embracer.embrace(opt) opt = opt or "default" return helper(opt.."?") end return embracer luacheck-0.21.1/spec/samples/indirect_globals.lua000066400000000000000000000001531315524664100217650ustar00rootroot00000000000000local t = table local g = global local t_concat t_concat = t.concat t_concat.foo.bar = g:method(g, global) luacheck-0.21.1/spec/samples/inline_options.lua000066400000000000000000000010421315524664100215100ustar00rootroot00000000000000-- luacheck: ignore 4 -- luacheck: ignore foo bar foo() bar() local function f(a) -- luacheck: no unused args -- luacheck: globals baz foo() bar() baz() qu() -- luacheck: globals qu qu() end baz() -- luacheck should ignore this comment -- luacheck: push ignore 2/f local f -- luacheck: push ignore 2/g local g -- luacheck: pop local f, g -- luacheck: pop local f, g -- luacheck: push local function f() --luacheck: ignore -- luacheck: pop end -- luacheck: ignore 5 do end -- luacheck: enable 54 do end if false then end luacheck-0.21.1/spec/samples/line_length.lua000066400000000000000000000030511315524664100207510ustar00rootroot00000000000000-- luacheck: only 63 local aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ------------------------------------------------------------------------------------------------------------------------------------------ comments can be long, too -- luacheck: push no max code line length aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = "readable code" string = [[ looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong ]] normal = "still normal" -- luacheck: push max line length 40 this_overrides_code_line_specific_option = true -- luacheck: pop -- luacheck: pop aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = "unreadable" -- luacheck: max line length 80 -- luacheck: max string line length 100 -- luacheck: max comment line length 120 local i_code_in_ed_in_a_terminal_with_default_width_and_i_dont_like_long_lines = true local really = false string2 = [[kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk94 ]] string3 = [[kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk105 ]] code() -- comment kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk105 --[[ comment kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk125 ]] luacheck-0.21.1/spec/samples/python_code.lua000066400000000000000000000000371315524664100207750ustar00rootroot00000000000000from __future__ import braces luacheck-0.21.1/spec/samples/read_globals.lua000066400000000000000000000001731315524664100211010ustar00rootroot00000000000000string = "foo" table.append = table.insert _ENV = nil foo = "4"; print(foo) bar = "5"; print(bar) baz[4] = "6"; print(baz) luacheck-0.21.1/spec/samples/read_globals_inline_options.lua000066400000000000000000000002731315524664100242130ustar00rootroot00000000000000-- luacheck: read globals foo bar foo(bar, baz) foo, bar, baz, baz[1] = false, true, nil, 5 -- luacheck: ignore 111/foo 121/ba. -- luacheck: globals bar baz foo, bar, baz = 678, 829, 914 luacheck-0.21.1/spec/samples/redefined.lua000066400000000000000000000002761315524664100204140ustar00rootroot00000000000000local a = {} function a:b(...) local a, self = 4 do local a = (...)(a) each(a, function() local self = self[5]; return self.bar end) end print(a[1]) end return a luacheck-0.21.1/spec/samples/sample.rockspec000066400000000000000000000002101315524664100207640ustar00rootroot00000000000000build = { type = "builtin", modules = { good = "spec/samples/good_code.lua", bad = "spec/samples/bad_code.lua" } } luacheck-0.21.1/spec/samples/unused_code.lua000066400000000000000000000004201315524664100207530ustar00rootroot00000000000000local foo = {} function foo.bar(baz) for i=1, 5 do local q for a, b, c in pairs(foo) do print(4) end end end local x = 5 x = 6 x = 7; print(x) local y = 5; (function() print(y) end)() y = 6 local z = 5; (function() z = 4 end)() z = 6 luacheck-0.21.1/spec/samples/unused_secondaries.lua000066400000000000000000000002001315524664100223340ustar00rootroot00000000000000local function f() end local a, b = f() f(b) local x, y, z = f(), f() f(y) local t, o = {} o = 5 print(o) o, t[1] = f() f(t) luacheck-0.21.1/spec/standards_spec.lua000066400000000000000000000177511315524664100200260ustar00rootroot00000000000000local standards = require "luacheck.standards" describe("standards", function() describe("validate_std_table", function() it("returns falsy value if argument is not a table", function() assert.falsy(standards.validate_std_table("foo")) end) it("returns falsy value if argument table has wrong field types", function() assert.falsy(standards.validate_std_table({globals = "all of them"})) assert.falsy(standards.validate_std_table({read_globals = "yes"})) end) it("returns falsy value if argument table has invalid definitions as values", function() assert.falsy(standards.validate_std_table({globals = {foo = "bar"}})) end) it("returns falsy value if argument table has invalid names as values", function() assert.falsy(standards.validate_std_table({globals = {12345}})) end) it("returns falsy value if definition tables have wrong field types", function() assert.falsy(standards.validate_std_table({globals = {foo = {read_only = "not_really"}}})) assert.falsy(standards.validate_std_table({read_globals = {bar = {other_fields = 0}}})) end) it("detects invalid nested definitions", function() assert.falsy(standards.validate_std_table({globals = {foo = {fields = {bar = 12345}}}})) end) it("returns thruthy value if argument std table is valid", function() assert.truthy(standards.validate_std_table({})) assert.truthy(standards.validate_std_table({unrelated = 123})) assert.truthy(standards.validate_std_table( {globals = {"foo", bar = {read_only = true, other_fields = false}}} )) end) end) describe("add_std_table", function() it("adds two empty stds", function() local fstd = {} standards.add_std_table(fstd, {}) assert.same({}, fstd) end) describe("when merging trees", function() local tree local std before_each(function() tree = { fields = { foo = { read_only = false, other_fields = true, fields = { nested = {read_only = true} } } } } std = { read_globals = { foo = { other_fields = false, fields = { nested = {other_fields = true}, nested2 = {} } } }, globals = {"bar"} } end) it("merges in a tree", function() standards.add_std_table(tree, std) assert.same({ fields = { foo = { read_only = false, other_fields = true, fields = { nested = {read_only = true, other_fields = true}, nested2 = {} } }, bar = {read_only = false, other_fields = true} } }, tree) end) it("merges in a tree and overwrites fields with overwrite = true", function() standards.add_std_table(tree, std, true) assert.same({ fields = { foo = { read_only = true, other_fields = false, fields = { nested = {read_only = true, other_fields = true}, nested2 = {} } }, bar = {read_only = false, other_fields = true} } }, tree) end) it("can ignore top-level array part of std", function() standards.add_std_table(tree, std, true, true) assert.same({ fields = { foo = { read_only = true, other_fields = false, fields = { nested = {read_only = true, other_fields = true}, nested2 = {} } } } }, tree) end) end) end) describe("overwrite_field", function() it("adds definition of a field if it does not exist", function() local tree = { fields = { foo = {} } } standards.overwrite_field(tree, {"foo", "bar"}, false) assert.same({ fields = { foo = { fields = { bar = {other_fields = true, read_only = false} } } } }, tree) end) it("overwrites existing definitions", function() local tree = { fields = { foo = { fields = { bar = {other_fields = false, read_only = false, fields = {k = {}}} } } } } standards.overwrite_field(tree, {"foo", "bar"}, true) assert.same({ fields = { foo = { fields = { bar = {other_fields = true, read_only = true} } } } }, tree) end) end) describe("remove_field", function() it("removes definition of a field if it exists", function() local tree = { fields = { foo = { fields = { bar = {other_fields = false, read_only = false}, baz = {} } } } } standards.remove_field(tree, {"foo", "bar"}) assert.same({ fields = { foo = { fields = { baz = {} } } } }, tree) end) it("does nothing of definition does not exist already", function() local tree = { fields = { foo = { fields = { bar = {other_fields = false, read_only = false} } } } } standards.remove_field(tree, {"foo", "baz"}) assert.same({ fields = { foo = { fields = { bar = {other_fields = false, read_only = false} } } } }, tree) end) end) describe("finalize", function() it("annotates nodes without writable fields with deep_read_only = true", function() local tree = { read_only = true, fields = { foo = { read_only = false, fields = { nested = {other_fields = true} } }, bar = { fields = {one = {other_fields = true}, another = {}} } } } standards.finalize(tree) assert.same({ read_only = true, fields = { foo = { read_only = false, fields = { nested = {other_fields = true} } }, bar = { deep_read_only = true, fields = {one = {deep_read_only = true, other_fields = true}, another = {deep_read_only = true}} } } }, tree) end) end) describe("def_fields", function() it("returns a definition table containing empty fields with given names", function() assert.same({ fields = { foo = {}, bar = {} } }, standards.def_fields("foo", "bar")) end) end) end) luacheck-0.21.1/spec/utils_spec.lua000066400000000000000000000154631315524664100172010ustar00rootroot00000000000000local utils = require "luacheck.utils" describe("utils", function() describe("read_file", function() it("returns contents of a file", function() assert.match("contents\r?\n", utils.read_file("spec/folder/foo")) end) it("removes UTF-8 BOM", function() assert.match("foo\r?\nbar\r?\n", utils.read_file("spec/folder/bom")) end) it("returns nil for non-existent paths", function() assert.is_nil(utils.read_file("spec/folder/non-existent")) end) it("returns nil for directories", function() assert.is_nil(utils.read_file("spec/folder")) end) end) describe("load", function() it("loads function in an environment", function() local f = utils.load("return g", {g = "foo"}) assert.is_function(f) assert.is_equal("foo", f()) end) it("returns nil on syntax error", function() assert.is_nil(utils.load("return return", {})) end) end) describe("load_config", function() it("loads config from a file and returns it", function() assert.same({foo = "bar"}, (utils.load_config("spec/folder/config"))) end) it("passes second argument as environment", function() local function bar() return "bar" end assert.same({ foo = "bar", bar = bar }, (utils.load_config("spec/folder/env_config", {bar = bar}))) end) it("returns nil, \"I/O\" for non-existent paths", function() local ok, err = utils.load_config("spec/folder/non-existent") assert.is_nil(ok) assert.equal("I/O", err) end) it("returns nil, \"syntax\" for configs with syntax errors", function() local ok, err = utils.load_config("spec/folder/bad_config") assert.is_nil(ok) assert.equal("syntax", err) end) it("returns nil, \"runtime\" for configs with run-time errors", function() local ok, err = utils.load_config("spec/folder/env_config") assert.is_nil(ok) assert.equal("runtime", err) end) end) describe("array_to_set", function() it("converts array to set and returns it", function() assert.same({foo = 3, bar = 2}, utils.array_to_set({"foo", "bar", "foo"})) end) end) describe("concat_arrays", function() it("returns concatenated arrays", function() assert.same({1, 2, 3, 4}, utils.concat_arrays({{}, {1}, {2, 3, 4}, {}})) end) end) describe("update", function() it("updates first table with entries from second", function() local t1 = {k1 = 1, k2 = 2} local t2 = {k2 = 3, k3 = 4} local ret = utils.update(t1, t2) assert.same({k1 = 1, k2 = 3, k3 = 4}, t1) assert.equal(t1, ret) end) end) describe("class", function() it("returns an object creator", function() local cl = utils.class() assert.is_table(cl) cl.field = "foo" local obj = cl() assert.is_table(obj) obj.field2 = "bar" assert.equal("foo", obj.field) assert.is_nil(cl.field2) end) it("calls __init on object creation", function() local cl = utils.class() cl.__init = spy.new(function() end) local obj = cl("foo", "bar") assert.spy(cl.__init).was_called(1) assert.spy(cl.__init).was_called_with(obj, "foo", "bar") end) end) describe("Stack", function() it("supports push/pop operations and top/size fields", function() local stack = utils.Stack() assert.equal(0, stack.size) assert.is_nil(stack.top) stack:push(7) stack:push(8) assert.equal(2, stack.size) assert.equal(8, stack.top) assert.equal(8, stack:pop()) assert.equal(1, stack.size) assert.equal(7, stack.top) stack:push(4) assert.equal(2, stack.size) assert.equal(4, stack.top) assert.equal(4, stack:pop()) assert.equal(7, stack:pop()) assert.equal(0, stack.size) assert.is_nil(stack.top) end) end) describe("try", function() it("returns true, original return values on success", function() local ok, ret1, ret2 = utils.try(function(x, y) return x*2, y*2 end, 1, 2) assert.is_true(ok) assert.equal(2, ret1) assert.equal(4, ret2) end) it("returns false, error wrapper on error", function() local ok, res = utils.try(function() error("foo", 0) end) assert.is_false(ok) assert.table(res) assert.equal(res.err, "foo") assert.string(res.traceback) end) it("does not wrap already wrapped errors", function() local orig_traceback local ok, res = utils.try(function() local _, orig_res = utils.try(function() error("foo", 0) end) orig_traceback = orig_res.traceback error(orig_res, 0) end) assert.is_false(ok) assert.table(res) assert.equal(res.err, "foo") assert.string(res.traceback) assert.equal(res.traceback, orig_traceback) end) end) describe("ripairs", function() it("returns reversed ipairs", function() local arr = {foo = "bar", 5, 6, 7} local iterated = {} for i, v in utils.ripairs(arr) do table.insert(iterated, {i, v}) end assert.same({{3, 7}, {2, 6}, {1, 5}}, iterated) end) end) describe("after", function() it("returns substring after match", function() assert.equal("foo bar: baz", utils.after("bar: foo bar: baz", "bar:%s*")) end) it("returns nil when there is no match", function() assert.is_nil(utils.after("bar: foo bar: baz", "baz:%s*")) end) end) describe("strip", function() it("returns string without whitespace on ends", function() assert.equal("foo bar", utils.strip("\tfoo bar\n ")) end) end) describe("split", function() it("without separator, returns non-whitespace substrings", function() assert.same({"foo", "bar", "baz"}, utils.split(" foo bar\n baz ")) end) it("with separator, returns substrings between them", function() assert.same({"", "foo", " bar", "", " baz "}, utils.split(",foo, bar,, baz ", ",")) end) end) describe("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})) end) end) end) luacheck-0.21.1/src/000077500000000000000000000000001315524664100141505ustar00rootroot00000000000000luacheck-0.21.1/src/luacheck/000077500000000000000000000000001315524664100157275ustar00rootroot00000000000000luacheck-0.21.1/src/luacheck/analyze.lua000066400000000000000000000404331315524664100201010ustar00rootroot00000000000000local core_utils = require "luacheck.core_utils" local utils = require "luacheck.utils" -- The main part of analysis is connecting assignments to locals or upvalues -- with accesses that may use the assigned value. -- Accesses and assignments are split into two groups based on whether they happen -- in the closure that defines subject local variable (main assignment, main access) -- or in some nested closure (closure assignment, closure access). -- To avoid false positives, it's assumed that a closure may be called at any point -- starting from expression that creates it. -- Additionally, all operations on upvalues are considered in bulk, as in, -- when a closure is called, it's assumed that any subset of its upvalue assignments -- and accesses may happen, in any order. -- Assignments and accesses are connected based on whether they can reach each other. -- A main assignment is connected with a main access when the assignment can reach the access. -- A main assignment is connected with a closure access when the assignment can reach the closure creation -- or the closure creation can reach the assignment. -- A closure assignment is connected with a main access when the closure creation can reach the access. -- A closure assignment is connected with a closure access when either closure creation can reach the other one. -- To determine what flow graph nodes an assignment or a closure creation can reach, -- they are independently propagated along the graph. -- Closure creation propagation is not bounded. -- Main assignment propagation is bounded by entrance and exit conditions for each reached flow graph node. -- Entrance condition checks that target local variable is still in scope. If entrance condition fails, -- nothing in the node can refer to the variable, and the scope can't be reentered later. -- So, in this case, assignment does not reach the node, and propagation does not continue. -- Exit condition checks that target local variable is not overwritten by an assignment in the node. -- If it fails, the assignment still reaches the node (because all accesses in a node are evaluated before any -- assignments take effect), but propagation does not continue. local function register_value(values_per_var, var, value) if not values_per_var[var] then values_per_var[var] = {} 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 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 externally_accessible(value) return value.type ~= "var" or (value.node and externally_accessible_tags[value.node.tag]) end local function find_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_overwriting_node_in_dup_assignment(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 -- Emits warnings for variable. local function check_var(chstate, var) if is_function_var(var) then local value = var.values[2] or var.values[1] if not value.used then chstate:warn_unused_variable(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 chstate:warn_unaccessed(var, true) end else chstate:warn_unused_variable(var.values[1], nil, nil, var.values[1].empty) end elseif var.values[1].empty then var.empty = true chstate:warn_unset(var) end elseif not var.accessed and not var.mutated then chstate:warn_unaccessed(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 chstate:warn_unaccessed(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 overwriting_node = find_overwriting_lhs_node(value.overwriting_item, value) if overwriting_node == value.node then overwriting_node = nil end else overwriting_node = get_overwriting_node_in_dup_assignment(value.item, value) end chstate:warn_unused_value(value, false, overwriting_node) elseif not value.used and not externally_accessible(value) then if var.accessed or not no_values_externally_accessible then chstate:warn_unused_value(value, true) end end end end end end -- Emits warnings for unused variables and values and unset variables in line. local function check_for_warnings(chstate, line) for _, item in ipairs(line.items) do if item.tag == "Local" then for var in pairs(item.set_variables) do -- Do not check implicit top level vararg. if var.location then check_var(chstate, var) end end end end end 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 unused recursive and mutually recursive functions. local function check_unused_recursive_funcs(chstate, line) -- 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. -- 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 simply recursive or mutually recursive. local simply_recursive = forward_edges[mut_rec_line][mut_rec_line] if is_function_var(value.var) then chstate:warn_unused_variable(value, true, simply_recursive) else chstate:warn_unused_value(value, true, simply_recursive) end end end end end end end -- Finds reaching assignments for all variable accesses. -- Emits warnings: unused variable, unused value, unset variable. local function analyze(chstate, line) analyze_line(line) for _, nested_line in ipairs(line.lines) do analyze_line(nested_line) end check_for_warnings(chstate, line) for _, nested_line in ipairs(line.lines) do check_for_warnings(chstate, nested_line) end check_unused_recursive_funcs(chstate, line) end return analyze luacheck-0.21.1/src/luacheck/argparse.lua000066400000000000000000000703231315524664100202430ustar00rootroot00000000000000-- The MIT License (MIT) -- Copyright (c) 2013 - 2015 Peter Melnichenko -- Permission is hereby granted, free of charge, to any person obtaining a copy of -- this software and associated documentation files (the "Software"), to deal in -- the Software without restriction, including without limitation the rights to -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -- the Software, and to permit persons to whom the Software is furnished to do so, -- subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all -- copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. local function deep_update(t1, t2) for k, v in pairs(t2) do if type(v) == "table" then v = deep_update({}, v) end t1[k] = v end return t1 end -- A property is a tuple {name, callback}. -- properties.args is number of properties that can be set as arguments -- when calling an object. local function class(prototype, properties, parent) -- Class is the metatable of its instances. local cl = {} cl.__index = cl if parent then cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) else cl.__prototype = prototype end if properties then local names = {} -- Create setter methods and fill set of property names. for _, property in ipairs(properties) do local name, callback = property[1], property[2] cl[name] = function(self, value) if not callback(self, value) then self["_" .. name] = value end return self end names[name] = true end function cl.__call(self, ...) -- When calling an object, if the first argument is a table, -- interpret keys as property names, else delegate arguments -- to corresponding setters in order. if type((...)) == "table" then for name, value in pairs((...)) do if names[name] then self[name](self, value) end end else local nargs = select("#", ...) for i, property in ipairs(properties) do if i > nargs or i > properties.args then break end local arg = select(i, ...) if arg ~= nil then self[property[1]](self, arg) end end end return self end end -- If indexing class fails, fallback to its parent. local class_metatable = {} class_metatable.__index = parent function class_metatable.__call(self, ...) -- Calling a class returns its instance. -- Arguments are delegated to the instance. local object = deep_update({}, self.__prototype) setmetatable(object, self) return object(...) end return setmetatable(cl, class_metatable) end local function typecheck(name, types, value) for _, type_ in ipairs(types) do if type(value) == type_ then return true end end error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) end local function typechecked(name, ...) local types = {...} return {name, function(_, value) typecheck(name, types, value) end} end local multiname = {"name", function(self, value) typecheck("name", {"string"}, value) for alias in value:gmatch("%S+") do self._name = self._name or alias table.insert(self._aliases, alias) end -- Do not set _name as with other properties. return true end} local function parse_boundaries(str) if tonumber(str) then return tonumber(str), tonumber(str) end if str == "*" then return 0, math.huge end if str == "+" then return 1, math.huge end if str == "?" then return 0, 1 end if str:match "^%d+%-%d+$" then local min, max = str:match "^(%d+)%-(%d+)$" return tonumber(min), tonumber(max) end if str:match "^%d+%+$" then local min = str:match "^(%d+)%+$" return tonumber(min), math.huge end end local function boundaries(name) return {name, function(self, value) typecheck(name, {"number", "string"}, value) local min, max = parse_boundaries(value) if not min then error(("bad property '%s'"):format(name)) end self["_min" .. name], self["_max" .. name] = min, max end} end local actions = {} local option_action = {"action", function(_, value) typecheck("action", {"function", "string"}, value) if type(value) == "string" and not actions[value] then error(("unknown action '%s'"):format(value)) end end} local option_init = {"init", function(self) self._has_init = true end} local option_default = {"default", function(self, value) if type(value) ~= "string" then self._init = value self._has_init = true return true end end} local add_help = {"add_help", function(self, value) typecheck("add_help", {"boolean", "string", "table"}, value) if self._has_help then table.remove(self._options) self._has_help = false end if value then local help = self:flag() :description "Show this help message and exit." :action(function() print(self:get_help()) os.exit(0) end) if value ~= true then help = help(value) end if not help._name then help "-h" "--help" end self._has_help = true end end} local Parser = class({ _arguments = {}, _options = {}, _commands = {}, _mutexes = {}, _require_command = true, _handle_options = true }, { args = 3, typechecked("name", "string"), typechecked("description", "string"), typechecked("epilog", "string"), typechecked("usage", "string"), typechecked("help", "string"), typechecked("require_command", "boolean"), typechecked("handle_options", "boolean"), typechecked("action", "function"), add_help }) local Command = class({ _aliases = {} }, { args = 3, multiname, typechecked("description", "string"), typechecked("epilog", "string"), typechecked("target", "string"), typechecked("usage", "string"), typechecked("help", "string"), typechecked("require_command", "boolean"), typechecked("handle_options", "boolean"), typechecked("action", "function"), add_help }, Parser) local Argument = class({ _minargs = 1, _maxargs = 1, _mincount = 1, _maxcount = 1, _defmode = "unused", _show_default = true }, { args = 5, typechecked("name", "string"), typechecked("description", "string"), option_default, typechecked("convert", "function", "table"), boundaries("args"), typechecked("target", "string"), typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("argname", "string", "table"), option_action, option_init }) local Option = class({ _aliases = {}, _mincount = 0, _overwrite = true }, { args = 6, multiname, typechecked("description", "string"), option_default, typechecked("convert", "function", "table"), boundaries("args"), boundaries("count"), typechecked("target", "string"), typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("overwrite", "boolean"), typechecked("argname", "string", "table"), option_action, option_init }, Argument) function Argument:_get_argument_list() local buf = {} local i = 1 while i <= math.min(self._minargs, 3) do local argname = self:_get_argname(i) if self._default and self._defmode:find "a" then argname = "[" .. argname .. "]" end table.insert(buf, argname) i = i+1 end while i <= math.min(self._maxargs, 3) do table.insert(buf, "[" .. self:_get_argname(i) .. "]") i = i+1 if self._maxargs == math.huge then break end end if i < self._maxargs then table.insert(buf, "...") end return buf end function Argument:_get_usage() local usage = table.concat(self:_get_argument_list(), " ") if self._default and self._defmode:find "u" then if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then usage = "[" .. usage .. "]" end end return usage end function actions.store_true(result, target) result[target] = true end function actions.store_false(result, target) result[target] = false end function actions.store(result, target, argument) result[target] = argument end function actions.count(result, target, _, overwrite) if not overwrite then result[target] = result[target] + 1 end end function actions.append(result, target, argument, overwrite) result[target] = result[target] or {} table.insert(result[target], argument) if overwrite then table.remove(result[target], 1) end end function actions.concat(result, target, arguments, overwrite) if overwrite then error("'concat' action can't handle too many invocations") end result[target] = result[target] or {} for _, argument in ipairs(arguments) do table.insert(result[target], argument) end end function Argument:_get_action() local action, init if self._maxcount == 1 then if self._maxargs == 0 then action, init = "store_true", nil else action, init = "store", nil end else if self._maxargs == 0 then action, init = "count", 0 else action, init = "append", {} end end if self._action then action = self._action end if self._has_init then init = self._init end if type(action) == "string" then action = actions[action] end return action, init end -- Returns placeholder for `narg`-th argument. function Argument:_get_argname(narg) local argname = self._argname or self:_get_default_argname() if type(argname) == "table" then return argname[narg] else return argname end end function Argument:_get_default_argname() return "<" .. self._name .. ">" end function Option:_get_default_argname() return "<" .. self:_get_default_target() .. ">" end -- Returns label to be shown in the help message. function Argument:_get_label() return self._name end function Option:_get_label() local variants = {} local argument_list = self:_get_argument_list() table.insert(argument_list, 1, nil) for _, alias in ipairs(self._aliases) do argument_list[1] = alias table.insert(variants, table.concat(argument_list, " ")) end return table.concat(variants, ", ") end function Command:_get_label() return table.concat(self._aliases, ", ") end function Argument:_get_description() if self._default and self._show_default then if self._description then return ("%s (default: %s)"):format(self._description, self._default) else return ("default: %s"):format(self._default) end else return self._description or "" end end function Command:_get_description() return self._description or "" end function Option:_get_usage() local usage = self:_get_argument_list() table.insert(usage, 1, self._name) usage = table.concat(usage, " ") if self._mincount == 0 or self._default then usage = "[" .. usage .. "]" end return usage end function Argument:_get_default_target() return self._name end function Option:_get_default_target() local res for _, alias in ipairs(self._aliases) do if alias:sub(1, 1) == alias:sub(2, 2) then res = alias:sub(3) break end end res = res or self._name:sub(2) return (res:gsub("-", "_")) end function Option:_is_vararg() return self._maxargs ~= self._minargs end function Parser:_get_fullname() local parent = self._parent local buf = {self._name} while parent do table.insert(buf, 1, parent._name) parent = parent._parent end return table.concat(buf, " ") end function Parser:_update_charset(charset) charset = charset or {} for _, command in ipairs(self._commands) do command:_update_charset(charset) end for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do charset[alias:sub(1, 1)] = true end end return charset end function Parser:argument(...) local argument = Argument(...) table.insert(self._arguments, argument) return argument end function Parser:option(...) local option = Option(...) if self._has_help then table.insert(self._options, #self._options, option) else table.insert(self._options, option) end return option end function Parser:flag(...) return self:option():args(0)(...) end function Parser:command(...) local command = Command():add_help(true)(...) command._parent = self table.insert(self._commands, command) return command end function Parser:mutex(...) local options = {...} for i, option in ipairs(options) do assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i)) end table.insert(self._mutexes, options) return self end local max_usage_width = 70 local usage_welcome = "Usage: " function Parser:get_usage() if self._usage then return self._usage end local lines = {usage_welcome .. self:_get_fullname()} local function add(s) if #lines[#lines]+1+#s <= max_usage_width then lines[#lines] = lines[#lines] .. " " .. s else lines[#lines+1] = (" "):rep(#usage_welcome) .. s end end -- This can definitely be refactored into something cleaner local mutex_options = {} local vararg_mutexes = {} -- First, put mutexes which do not contain vararg options and remember those which do for _, mutex in ipairs(self._mutexes) do local buf = {} local is_vararg = false for _, option in ipairs(mutex) do if option:_is_vararg() then is_vararg = true end table.insert(buf, option:_get_usage()) mutex_options[option] = true end local repr = "(" .. table.concat(buf, " | ") .. ")" if is_vararg then table.insert(vararg_mutexes, repr) else add(repr) end end -- Second, put regular options for _, option in ipairs(self._options) do if not mutex_options[option] and not option:_is_vararg() then add(option:_get_usage()) end end -- Put positional arguments for _, argument in ipairs(self._arguments) do add(argument:_get_usage()) end -- Put mutexes containing vararg options for _, mutex_repr in ipairs(vararg_mutexes) do add(mutex_repr) end for _, option in ipairs(self._options) do if not mutex_options[option] and option:_is_vararg() then add(option:_get_usage()) end end if #self._commands > 0 then if self._require_command then add("") else add("[]") end add("...") end return table.concat(lines, "\n") end local margin_len = 3 local margin_len2 = 25 local margin = (" "):rep(margin_len) local margin2 = (" "):rep(margin_len2) local function make_two_columns(s1, s2) if s2 == "" then return margin .. s1 end s2 = s2:gsub("\n", "\n" .. margin2) if #s1 < (margin_len2-margin_len) then return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2 else return margin .. s1 .. "\n" .. margin2 .. s2 end end function Parser:get_help() if self._help then return self._help end local blocks = {self:get_usage()} if self._description then table.insert(blocks, self._description) end local labels = {"Arguments:", "Options:", "Commands:"} for i, elements in ipairs{self._arguments, self._options, self._commands} do if #elements > 0 then local buf = {labels[i]} for _, element in ipairs(elements) do table.insert(buf, make_two_columns(element:_get_label(), element:_get_description())) end table.insert(blocks, table.concat(buf, "\n")) end end if self._epilog then table.insert(blocks, self._epilog) end return table.concat(blocks, "\n\n") end local function get_tip(context, wrong_name) local context_pool = {} local possible_name local possible_names = {} for name in pairs(context) do if type(name) == "string" then for i = 1, #name do possible_name = name:sub(1, i - 1) .. name:sub(i + 1) if not context_pool[possible_name] then context_pool[possible_name] = {} end table.insert(context_pool[possible_name], name) end end end for i = 1, #wrong_name + 1 do possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) if context[possible_name] then possible_names[possible_name] = true elseif context_pool[possible_name] then for _, name in ipairs(context_pool[possible_name]) do possible_names[name] = true end end end local first = next(possible_names) if first then if next(possible_names, first) then local possible_names_arr = {} for name in pairs(possible_names) do table.insert(possible_names_arr, "'" .. name .. "'") end table.sort(possible_names_arr) return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" else return "\nDid you mean '" .. first .. "'?" end else return "" end end local ElementState = class({ invocations = 0 }) function ElementState:__call(state, element) self.state = state self.result = state.result self.element = element self.target = element._target or element:_get_default_target() self.action, self.result[self.target] = element:_get_action() return self end function ElementState:error(fmt, ...) self.state:error(fmt, ...) end function ElementState:convert(argument) local converter = self.element._convert if converter then local ok, err if type(converter) == "function" then ok, err = converter(argument) else ok = converter[argument] end if ok == nil then self:error(err and "%s" or "malformed argument '%s'", err or argument) end argument = ok end return argument end function ElementState:default(mode) return self.element._defmode:find(mode) and self.element._default end local function bound(noun, min, max, is_max) local res = "" if min ~= max then res = "at " .. (is_max and "most" or "least") .. " " end local number = is_max and max or min return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") end function ElementState:invoke(alias) self.open = true self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) self.overwrite = false if self.invocations >= self.element._maxcount then if self.element._overwrite then self.overwrite = true else self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true)) end else self.invocations = self.invocations + 1 end self.args = {} if self.element._maxargs <= 0 then self:close() end return self.open end function ElementState:pass(argument) argument = self:convert(argument) table.insert(self.args, argument) if #self.args >= self.element._maxargs then self:close() end return self.open end function ElementState:complete_invocation() while #self.args < self.element._minargs do self:pass(self.element._default) end end function ElementState:close() if self.open then self.open = false if #self.args < self.element._minargs then if self:default("a") then self:complete_invocation() else if #self.args == 0 then if getmetatable(self.element) == Argument then self:error("missing %s", self.name) elseif self.element._maxargs == 1 then self:error("%s requires an argument", self.name) end end self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) end end local args = self.args if self.element._maxargs <= 1 then args = args[1] end if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then args = self.args end self.action(self.result, self.target, args, self.overwrite) end end local ParseState = class({ result = {}, options = {}, arguments = {}, argument_i = 1, element_to_mutexes = {}, mutex_to_used_option = {}, command_actions = {} }) function ParseState:__call(parser, error_handler) self.parser = parser self.error_handler = error_handler self.charset = parser:_update_charset() self:switch(parser) return self end function ParseState:error(fmt, ...) self.error_handler(self.parser, fmt:format(...)) end function ParseState:switch(parser) self.parser = parser if parser._action then table.insert(self.command_actions, parser._action) end for _, option in ipairs(parser._options) do option = ElementState(self, option) table.insert(self.options, option) for _, alias in ipairs(option.element._aliases) do self.options[alias] = option end end for _, mutex in ipairs(parser._mutexes) do for _, option in ipairs(mutex) do if not self.element_to_mutexes[option] then self.element_to_mutexes[option] = {} end table.insert(self.element_to_mutexes[option], mutex) end end for _, argument in ipairs(parser._arguments) do argument = ElementState(self, argument) table.insert(self.arguments, argument) argument:invoke() end self.handle_options = parser._handle_options self.argument = self.arguments[self.argument_i] self.commands = parser._commands for _, command in ipairs(self.commands) do for _, alias in ipairs(command._aliases) do self.commands[alias] = command end end end function ParseState:get_option(name) local option = self.options[name] if not option then self:error("unknown option '%s'%s", name, get_tip(self.options, name)) else return option end end function ParseState:get_command(name) local command = self.commands[name] if not command then if #self.commands > 0 then self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) else self:error("too many arguments") end else return command end end function ParseState:invoke(option, name) self:close() if self.element_to_mutexes[option.element] then for _, mutex in ipairs(self.element_to_mutexes[option.element]) do local used_option = self.mutex_to_used_option[mutex] if used_option and used_option ~= option then self:error("option '%s' can not be used together with %s", name, used_option.name) else self.mutex_to_used_option[mutex] = option end end end if option:invoke(name) then self.option = option end end function ParseState:pass(arg) if self.option then if not self.option:pass(arg) then self.option = nil end elseif self.argument then if not self.argument:pass(arg) then self.argument_i = self.argument_i + 1 self.argument = self.arguments[self.argument_i] end else local command = self:get_command(arg) self.result[command._target or command._name] = true self:switch(command) end end function ParseState:close() if self.option then self.option:close() self.option = nil end end function ParseState:finalize() self:close() for i = self.argument_i, #self.arguments do local argument = self.arguments[i] if #argument.args == 0 and argument:default("u") then argument:complete_invocation() else argument:close() end end if self.parser._require_command and #self.commands > 0 then self:error("a command is required") end for _, option in ipairs(self.options) do local name = option.name or ("option '%s'"):format(option.element._name) if option.invocations == 0 then if option:default("u") then option:invoke(name) option:complete_invocation() option:close() end end local mincount = option.element._mincount if option.invocations < mincount then if option:default("a") then while option.invocations < mincount do option:invoke(name) option:close() end elseif option.invocations == 0 then self:error("missing %s", name) else self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount)) end end end for i = #self.command_actions, 1, -1 do self.command_actions[i](self.result) end end function ParseState:parse(args) for _, arg in ipairs(args) do local plain = true if self.handle_options then local first = arg:sub(1, 1) if self.charset[first] then if #arg > 1 then plain = false if arg:sub(2, 2) == first then if #arg == 2 then self:close() self.handle_options = false else local equals = arg:find "=" if equals then local name = arg:sub(1, equals - 1) local option = self:get_option(name) if option.element._maxargs <= 0 then self:error("option '%s' does not take arguments", name) end self:invoke(option, name) self:pass(arg:sub(equals + 1)) else local option = self:get_option(arg) self:invoke(option, arg) end end else for i = 2, #arg do local name = first .. arg:sub(i, i) local option = self:get_option(name) self:invoke(option, name) if i ~= #arg and option.element._maxargs > 0 then self:pass(arg:sub(i + 1)) break end end end end end end if plain then self:pass(arg) end end self:finalize() return self.result end function Parser:error(msg) io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) os.exit(1) end -- Compatibility with strict.lua and other checkers: local default_cmdline = rawget(_G, "arg") or {} function Parser:_parse(args, error_handler) return ParseState(self, error_handler):parse(args or default_cmdline) end function Parser:parse(args) return self:_parse(args, self.error) end local function xpcall_error_handler(err) return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) end function Parser:pparse(args) local parse_error local ok, result = xpcall(function() return self:_parse(args, function(_, err) parse_error = err error(err, 0) end) end, xpcall_error_handler) if ok then return true, result elseif not parse_error then error(result, 0) else return false, parse_error end end return function(...) return Parser(default_cmdline[0]):add_help(true)(...) end luacheck-0.21.1/src/luacheck/builtin_standards.lua000066400000000000000000000215061315524664100221470ustar00rootroot00000000000000local standards = require "luacheck.standards" local builtin_standards = {} local function def_to_std(def) return {read_globals = def.fields} end local function add_defs(...) local res = {} for _, def in ipairs({...}) do standards.add_std_table(res, def_to_std(def)) end return res end local empty = {} local string_defs = {} string_defs.min = standards.def_fields("byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", "reverse", "sub", "upper") string_defs.lua51 = add_defs(string_defs.min, standards.def_fields("gfind")) string_defs.lua52 = string_defs.min string_defs.lua53 = add_defs(string_defs.min, standards.def_fields("pack", "packsize", "unpack")) string_defs.luajit = string_defs.lua51 local file_defs = {} file_defs.min = { fields = { __gc = empty, __index = {other_fields = true}, __tostring = empty, close = empty, flush = empty, lines = empty, read = empty, seek = empty, setvbuf = empty, write = empty } } file_defs.lua51 = file_defs.min file_defs.lua52 = file_defs.min file_defs.lua53 = add_defs(file_defs.min, {fields = {__name = string_defs.lua53}}) file_defs.luajit = file_defs.min local function make_min_def(method_defs) local string_def = string_defs[method_defs] local file_def = file_defs[method_defs] return { fields = { _G = {other_fields = true, read_only = false}, _VERSION = string_def, arg = {other_fields = true}, assert = empty, collectgarbage = empty, coroutine = standards.def_fields("create", "resume", "running", "status", "wrap", "yield"), debug = standards.def_fields("debug", "gethook", "getinfo", "getlocal", "getmetatable", "getregistry", "getupvalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback"), dofile = empty, error = empty, getmetatable = empty, io = { fields = { close = empty, flush = empty, input = empty, lines = empty, open = empty, output = empty, popen = empty, read = empty, stderr = file_def, stdin = file_def, stdout = file_def, tmpfile = empty, type = empty, write = empty } }, ipairs = empty, load = empty, loadfile = empty, math = standards.def_fields("abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "floor", "fmod", "huge", "log", "max", "min", "modf", "pi", "rad", "random", "randomseed", "sin", "sqrt", "tan"), next = empty, os = standards.def_fields("clock", "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname"), package = { fields = { config = string_def, cpath = {fields = string_def.fields, read_only = false}, loaded = {other_fields = true, read_only = false}, loadlib = empty, path = {fields = string_def.fields, read_only = false}, preload = {other_fields = true, read_only = false} } }, pairs = empty, pcall = empty, print = empty, rawequal = empty, rawget = empty, rawset = empty, require = empty, select = empty, setmetatable = empty, string = string_def, table = standards.def_fields("concat", "insert", "remove", "sort"), tonumber = empty, tostring = empty, type = empty, xpcall = empty } } end local bit32_def = standards.def_fields("arshift", "band", "bnot", "bor", "btest", "bxor", "extract", "lrotate", "lshift", "replace", "rrotate", "rshift") local lua_defs = {} lua_defs.min = make_min_def("min") lua_defs.lua51 = add_defs(make_min_def("lua52"), { fields = { debug = standards.def_fields("getfenv", "setfenv"), getfenv = empty, loadstring = empty, math = standards.def_fields("atan2", "cosh", "frexp", "ldexp", "log10", "pow", "sinh", "tanh"), module = empty, newproxy = empty, package = { fields = { loaders = {other_fields = true, read_only = false}, seeall = empty } }, setfenv = empty, table = standards.def_fields("maxn"), unpack = empty } }) lua_defs.lua51c = add_defs(lua_defs.lua51, make_min_def("lua51"), { fields = { gcinfo = empty, math = standards.def_fields("mod"), table = standards.def_fields("foreach", "foreachi", "getn", "setn") } }) lua_defs.lua52 = add_defs(make_min_def("lua52"), { fields = { _ENV = {other_fields = true, read_only = false}, bit32 = bit32_def, debug = standards.def_fields("getuservalue", "setuservalue", "upvalueid", "upvaluejoin"), math = standards.def_fields("atan2", "cosh", "frexp", "ldexp", "pow", "sinh", "tanh"), package = { fields = { searchers = {other_fields = true, read_only = false}, searchpath = empty } }, rawlen = empty, table = standards.def_fields("pack", "unpack") } }) lua_defs.lua52c = add_defs(lua_defs.lua52, { fields = { loadstring = empty, math = standards.def_fields("log10"), module = empty, package = { fields = { loaders = {other_fields = true, read_only = false}, seeall = empty } }, table = standards.def_fields("maxn"), unpack = empty } }) lua_defs.lua53 = add_defs(make_min_def("lua53"), { fields = { _ENV = {other_fields = true, read_only = false}, coroutine = standards.def_fields("isyieldable"), debug = standards.def_fields("getuservalue", "setuservalue", "upvalueid", "upvaluejoin"), math = standards.def_fields("maxinteger", "mininteger", "tointeger", "type", "ult"), package = { fields = { searchers = {other_fields = true, read_only = false}, searchpath = empty } }, rawlen = empty, table = standards.def_fields("move", "pack", "unpack"), utf8 = { fields = { char = empty, charpattern = string_defs.lua53, codepoint = empty, codes = empty, len = empty, offset = empty } } } }) lua_defs.lua53c = add_defs(lua_defs.lua53, { fields = { bit32 = bit32_def, math = standards.def_fields("atan2", "cosh", "frexp", "ldexp", "log10", "pow", "sinh", "tanh") } }) lua_defs.luajit = add_defs(make_min_def("luajit"), { fields = { bit = standards.def_fields("arshift", "band", "bnot", "bor", "bswap", "bxor", "lshift", "rol", "ror", "rshift", "tobit", "tohex"), coroutine = standards.def_fields("isyieldable"), debug = standards.def_fields("getfenv", "setfenv", "upvalueid", "upvaluejoin"), gcinfo = empty, getfenv = empty, jit = {other_fields = true}, loadstring = empty, math = standards.def_fields("atan2", "cosh", "frexp", "ldexp", "log10", "mod", "pow", "sinh", "tanh"), module = empty, newproxy = empty, package = { fields = { loaders = {other_fields = true, read_only = false}, searchpath = empty, seeall = empty } }, setfenv = empty, table = standards.def_fields("clear", "foreach", "foreachi", "getn", "maxn", "move", "new"), unpack = empty } }) lua_defs.ngx_lua = add_defs(lua_defs.luajit, require "luacheck.ngx_standard") lua_defs.max = add_defs(lua_defs.lua51c, lua_defs.lua52c, lua_defs.lua53c, lua_defs.luajit) for name, def in pairs(lua_defs) do builtin_standards[name] = def_to_std(def) end local function detect_default_std() if rawget(_G, "jit") then return "luajit" elseif _VERSION == "Lua 5.1" then return "lua51c" elseif _VERSION == "Lua 5.2" then return "lua52c" elseif _VERSION == "Lua 5.3" then return "lua53c" else return "max" end end builtin_standards._G = builtin_standards[detect_default_std()] builtin_standards.busted = { read_globals = { "describe", "insulate", "expose", "it", "pending", "before_each", "after_each", "lazy_setup", "lazy_teardown", "strict_setup", "strict_teardown", "setup", "teardown", "context", "spec", "test", "assert", "spy", "mock", "stub", "finally" } } builtin_standards.love = require "luacheck.love_standard" builtin_standards.rockspec = { globals = { "rockspec_format", "package", "version", "description", "supported_platforms", "dependencies", "external_dependencies", "source", "build" } } builtin_standards.none = {} return builtin_standards luacheck-0.21.1/src/luacheck/cache.lua000066400000000000000000000223751315524664100175060ustar00rootroot00000000000000local utils = require "luacheck.utils" local cache = {} -- Cache file contains check results for n unique filenames. -- Cache file consists of 3n+2 lines, the first line is empty and the second is cache format version. -- The rest are contain file records, 3 lines per file. -- For each file, first line is the filename, second is modification time, -- third is check result in lua table format. -- String fields are compressed into array indexes. cache.format_version = 22 local option_fields = { "ignore", "std", "globals", "unused_args", "self", "compat", "global", "unused", "redefined", "unused_secondaries", "allow_defined", "allow_defined_top", "module", "read_globals", "new_globals", "new_read_globals", "enable", "only", "not_globals", "max_line_length", "max_code_line_length", "max_string_line_length", "max_comment_line_length" } local event_fields = { "code", "name", "line", "column", "end_column", "prev_line", "prev_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" } -- Recursively replace string keys with integer keys. local function compress(t, fields) fields = fields or event_fields local res = {} for index, field in ipairs(fields) do local value = t[field] if value ~= nil then if field == "options" then value = compress(value, option_fields) end res[index] = value end end return res end local function compress_report(report) 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) end 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 res = {} for index, field in ipairs(fields) do local value = t[index] if value ~= nil then if field == "options" then value = decompress(value, option_fields) end res[field] = value end end return res end local function decompress_report(compressed) local report = {} report.events = utils.map(decompress, compressed[1]) report.per_line_options = {} for line, events in pairs(compressed[2]) do report.per_line_options[line] = utils.map(decompress, events) end report.line_lengths = compressed[3] report.line_endings = compressed[4] return report end local function get_local_name(index) return string.char(index + (index > 26 and 70 or 64)) end local function max_n(t) local res = 0 for k in pairs(t) do res = math.max(res, k) end return res end -- Serializes a value into buffer. -- `strings` is a table mapping string values to where they first occured or to name of local -- variable used to represent it. -- Array part contains representations of values saved into locals. local function add_value(buffer, strings, value) if type(value) == "string" then local prev = strings[value] if type(prev) == "string" then -- There is a local with such value. table.insert(buffer, prev) elseif type(prev) == "number" and #strings < 52 then -- Value is used second time, put it into a local. table.insert(strings, ("%q"):format(value)) local local_name = get_local_name(#strings) buffer[prev] = local_name table.insert(buffer, local_name) strings[value] = local_name else table.insert(buffer, ("%q"):format(value)) strings[value] = #buffer end elseif type(value) == "table" then local is_sparse local put_one table.insert(buffer, "{") for i = 1, max_n(value) do local item = value[i] if item == nil then is_sparse = true else if put_one then table.insert(buffer, ",") end if is_sparse then table.insert(buffer, ("[%d]="):format(i)) end add_value(buffer, strings, item) put_one = true end end table.insert(buffer, "}") else table.insert(buffer, tostring(value)) end end -- Serializes check result into a string. function cache.serialize(report) local strings = {} local buffer = {"", "return "} add_value(buffer, strings, compress_report(report)) if strings[1] then local names = {} for index in ipairs(strings) do table.insert(names, get_local_name(index)) end buffer[1] = "local " .. table.concat(names, ",") .. "=" .. table.concat(strings, ",") .. ";" end return table.concat(buffer) end -- Returns array of triplets of lines from cache fh. local function read_triplets(fh) local res = {} while true do local filename = fh:read() if filename then local mtime = fh:read() or "" local cached = fh:read() or "" table.insert(res, {filename, mtime, cached}) else break end end return res end -- Writes cache triplets into fh. local function write_triplets(fh, triplets) for _, triplet in ipairs(triplets) do fh:write(triplet[1], "\n") fh:write(triplet[2], "\n") fh:write(triplet[3], "\n") end end -- Loads cached checking result from string, returns result or nil. local function load_cached(cached) local func = utils.load(cached, {}) if not func then return end local ok, res = pcall(func) if not ok then return end if type(res) == "table" then return decompress_report(res) end end local function check_version_header(fh) local first_line = fh:read() return (first_line == "" or first_line == "\r") and tonumber(fh:read()) == cache.format_version end local function write_version_header(fh) fh:write("\n", tostring(cache.format_version), "\n") end -- Loads cache for filenames given mtimes from cache cache_filename. -- Returns table mapping filenames to cached check results. -- On corrupted cache returns nil, on version mismatch returns {}. function cache.load(cache_filename, filenames, mtimes) local fh = io.open(cache_filename, "rb") if not fh then return {} end if not check_version_header(fh) then fh:close() return {} end local result = {} local not_yet_found = utils.array_to_set(filenames) while next(not_yet_found) do local filename = fh:read() if not filename then fh:close() return result end if filename:sub(-1) == "\r" then filename = filename:sub(1, -2) end local mtime = fh:read() local cached = fh:read() if not mtime or not cached then fh:close() return end mtime = tonumber(mtime) if not mtime then fh:close() return end if not_yet_found[filename] then if mtimes[not_yet_found[filename]] == mtime then result[filename] = load_cached(cached) if result[filename] == nil then fh:close() return end end not_yet_found[filename] = nil end end fh:close() return result end -- Updates cache at cache_filename with results for filenames. -- Returns success flag + whether update was append-only. function cache.update(cache_filename, filenames, mtimes, results) local old_triplets = {} local can_append = false local fh = io.open(cache_filename, "rb") if fh then if check_version_header(fh) then old_triplets = read_triplets(fh) can_append = true end fh:close() end local filename_set = utils.array_to_set(filenames) local old_filename_set = {} -- Update old cache for files which got a new result. for i, triplet in ipairs(old_triplets) do old_filename_set[triplet[1]] = true local file_index = filename_set[triplet[1]] if file_index then can_append = false old_triplets[i][2] = mtimes[file_index] old_triplets[i][3] = cache.serialize(results[file_index]) end end local new_triplets = {} for _, filename in ipairs(filenames) do -- Use unique index (there could be duplicate filenames). local file_index = filename_set[filename] if file_index and not old_filename_set[filename] then table.insert(new_triplets, { filename, mtimes[file_index], cache.serialize(results[file_index]) }) -- Do not save result for this filename again. filename_set[filename] = nil end end if can_append then if #new_triplets > 0 then fh = io.open(cache_filename, "ab") if not fh then return false end write_triplets(fh, new_triplets) fh:close() end else fh = io.open(cache_filename, "wb") if not fh then return false end write_version_header(fh) write_triplets(fh, old_triplets) write_triplets(fh, new_triplets) fh:close() end return true, can_append end return cache luacheck-0.21.1/src/luacheck/check.lua000066400000000000000000000201051315524664100175050ustar00rootroot00000000000000local parser = require "luacheck.parser" local linearize = require "luacheck.linearize" local analyze = require "luacheck.analyze" local reachability = require "luacheck.reachability" local inline_options = require "luacheck.inline_options" local utils = require "luacheck.utils" local check_whitespace = require "luacheck.whitespace" local detect_globals = require "luacheck.detect_globals" local function is_secondary(value) return value.secondaries and value.secondaries.used end local ChState = utils.class() function ChState:__init() self.warnings = {} end function ChState:warn(warning, implicit_self) if not warning.end_column then warning.end_column = implicit_self and warning.column or (warning.column + #warning.name - 1) end table.insert(self.warnings, warning) end local action_codes = { set = "1", mutate = "2", access = "3" } local type_codes = { var = "1", func = "1", arg = "2", loop = "3", loopi = "3" } -- `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`). function ChState:warn_global(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 -- and filter out the warning if the base of last indexing is already -- undefined and has been reported. -- E.g. avoid useless warning in the second statement of `local t = tabell; t.concat(...)`. self:warn({ 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 -- W12* (read-only global) and W131 (unused global) are patched in during filtering. function ChState:warn_unused_variable(value, recursive, self_recursive, useless) self:warn({ code = "21" .. type_codes[value.var.type], name = value.var.name, line = value.location.line, column = value.location.column, secondary = is_secondary(value) or nil, func = (value.type == "func") or nil, mutually_recursive = not self_recursive and recursive or nil, recursive = self_recursive, self = value.var.self, useless = value.var.name == "_" and useless or nil }, value.var.self) end function ChState:warn_unset(var) self:warn({ code = "221", name = var.name, line = var.location.line, column = var.location.column }) end function ChState:warn_unaccessed(var, 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 self:warn({ code = "2" .. (mutated and "4" or "3") .. type_codes[var.type], name = var.name, line = var.location.line, column = var.location.column, secondary = secondary }, var.self) end function ChState:warn_unused_value(value, mutated, overwriting_node) self:warn({ code = "3" .. (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, line = value.location.line, column = value.location.column, secondary = is_secondary(value) or nil, }, value.type == "arg" and value.var.self) end function ChState:warn_unused_field_value(node, overwriting_node) self:warn({ code = "314", field = node.field, index = node.is_index, overwritten_line = overwriting_node.location.line, overwritten_column = overwriting_node.location.column, line = node.location.line, column = node.location.column, end_column = node.location.column + #node.first_token - 1 }) end function ChState:warn_uninit(node, mutation) self:warn({ code = mutation and "341" or "321", name = node[1], line = node.location.line, column = node.location.column }) end function ChState:warn_redefined(var, prev_var, same_scope) if var.name ~= "..." then self:warn({ code = "4" .. (same_scope and "1" or (var.line == prev_var.line and "2" or "3")) .. type_codes[prev_var.type], name = var.name, line = var.location.line, column = var.location.column, self = var.self and prev_var.self, prev_line = prev_var.location.line, prev_column = prev_var.location.column }, var.self) end end function ChState:warn_unreachable(location, unrepeatable, token) self:warn({ code = "51" .. (unrepeatable and "2" or "1"), line = location.line, column = location.column, end_column = location.column + #token - 1 }) end function ChState:warn_unused_label(label) self:warn({ code = "521", label = label.name, line = label.location.line, column = label.location.column, end_column = label.end_column }) end function ChState:warn_unbalanced(location, shorter_lhs) -- Location points to `=`. self:warn({ code = "53" .. (shorter_lhs and "1" or "2"), line = location.line, column = location.column, end_column = location.column }) end function ChState:warn_empty_block(location, do_end) -- Location points to `do`, `then` or `else`. self:warn({ code = "54" .. (do_end and "1" or "2"), line = location.line, column = location.column, end_column = location.column + (do_end and 1 or 3) }) end function ChState:warn_empty_statement(location) self:warn({ code = "551", line = location.line, column = location.column, end_column = location.column }) end local function check_or_throw(src) local ast, comments, code_lines, line_endings, semicolons = parser.parse(src) local chstate = ChState() local line = linearize(chstate, ast) for _, location in ipairs(semicolons) do chstate:warn_empty_statement(location) end local lines = utils.split_lines(src) local line_lengths = utils.map(function(s) return #s end, lines) check_whitespace(chstate, lines, line_endings) analyze(chstate, line) reachability(chstate, line) detect_globals(chstate, line) local events, per_line_opts = inline_options.get_events(ast, comments, code_lines, chstate.warnings) return {events = events, per_line_options = per_line_opts, line_lengths = line_lengths, 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). -- `per_line_options`: map from line numbers to arrays of inline option events. local function check(src) local ok, res = utils.try(check_or_throw, src) if ok then return res elseif utils.is_instance(res.err, parser.SyntaxError) then local syntax_error = { code = "011", line = res.err.line, column = res.err.column, end_column = res.err.end_column, msg = res.err.msg } return {events = {syntax_error}, per_line_options = {}, line_lengths = {}} else error(res, 0) end end return check luacheck-0.21.1/src/luacheck/config.lua000066400000000000000000000216301315524664100177010ustar00rootroot00000000000000local options = require "luacheck.options" local builtin_standards = require "luacheck.builtin_standards" local fs = require "luacheck.fs" local globbing = require "luacheck.globbing" local utils = require "luacheck.utils" local config = {} -- Config must support special metatables for some keys: -- autovivification for `files`, fallback to built-in stds for `stds`. local special_mts = { stds = {__index = builtin_standards}, files = {__index = function(files, key) files[key] = {} return files[key] end} } local function make_config_env_mt() local env_mt = {} local special_values = {} for key, mt in pairs(special_mts) do special_values[key] = setmetatable({}, mt) end function env_mt.__index(_, key) if special_mts[key] then return special_values[key] else return _G[key] end end function env_mt.__newindex(env, key, value) if special_mts[key] then if type(value) == "table" then setmetatable(value, special_mts[key]) end special_values[key] = value else rawset(env, key, value) end end return env_mt, special_values end local function make_config_env() local mt, special_values = make_config_env_mt() return setmetatable({}, mt), special_values end local function remove_env_mt(env, special_values) setmetatable(env, nil) utils.update(env, special_values) end local top_options = { color = utils.has_type("boolean"), codes = utils.has_type("boolean"), formatter = utils.either(utils.has_type("string"), utils.has_type("function")), cache = utils.either(utils.has_type("string"), utils.has_type("boolean")), jobs = function(x) return type(x) == "number" and math.floor(x) == x and x >= 1 end, files = utils.has_type("table"), stds = utils.has_type("table"), exclude_files = utils.array_of("string"), include_files = utils.array_of("string") } utils.update(top_options, options.all_options) options.add_order(top_options) -- Returns error or nil if options are valid. local function validate_options(option_set, opts) local ok, invalid_field = options.validate(option_set, opts) if not ok then if invalid_field then return ("invalid value of option '%s'"):format(invalid_field) else return "validation error" end end end -- Returns error or nil if config is valid. local function validate_config(conf) local top_err = validate_options(top_options, conf) if top_err then return top_err end for path, opts in pairs(conf.files) do if type(path) == "string" then local override_err = validate_options(options.all_options, opts) if override_err then return ("%s in options for path '%s'"):format(override_err, path) end end end end -- Returns table with field `globs` containing sorted normalized globs -- used in overrides and `options` mapping these globs to options. local function normalize_overrides(files, abs_conf_dir) local overrides = {globs = {}, options = {}} local orig_globs = {} for glob in pairs(files) do table.insert(orig_globs, glob) end table.sort(orig_globs) for _, orig_glob in ipairs(orig_globs) do local glob = fs.normalize(fs.join(abs_conf_dir, orig_glob)) if not overrides.options[glob] then table.insert(overrides.globs, glob) end overrides.options[glob] = files[orig_glob] end table.sort(overrides.globs, globbing.compare) return overrides end local function try_load(path) local src = utils.read_file(path) if not src then return end local func, err = utils.load(src, nil, "@"..path) return err or func end local function add_relative_loader(conf) local function loader(modname) local modpath = fs.join(conf.rel_dir, (modname:gsub("%.", utils.dir_sep))) return try_load(modpath..".lua") or try_load(modpath..utils.dir_sep.."init.lua"), modname end table.insert(package.loaders or package.searchers, 1, loader) -- luacheck: compat return loader end local function remove_relative_loader(loader) for i, func in ipairs(package.loaders or package.searchers) do -- luacheck: compat if func == loader then table.remove(package.loaders or package.searchers, i) -- luacheck: compat return end end end local function get_global_config_dir() if utils.is_windows then local local_app_data_dir = os.getenv("LOCALAPPDATA") if not local_app_data_dir then local user_profile_dir = os.getenv("USERPROFILE") if user_profile_dir then local_app_data_dir = fs.join(user_profile_dir, "Local Settings", "Application Data") end end if local_app_data_dir then return fs.join(local_app_data_dir, "Luacheck") end else local fh = assert(io.popen("uname -s")) local system = fh:read("*l") fh:close() if system == "Darwin" then local home_dir = os.getenv("HOME") if home_dir then return fs.join(home_dir, "Library", "Application Support", "Luacheck") end else local config_home_dir = os.getenv("XDG_CONFIG_HOME") if not config_home_dir then local home_dir = os.getenv("HOME") if home_dir then config_home_dir = fs.join(home_dir, ".config") end end if config_home_dir then return fs.join(config_home_dir, "luacheck") end end end end config.default_path = ".luacheckrc" function config.get_default_global_path() local global_config_dir = get_global_config_dir() if global_config_dir then return fs.join(global_config_dir, config.default_path) end end config.empty_config = {empty = true} -- Loads config from path, returns config object or nil and error message. function config.load_config(path, global_path) local is_default_path = not path path = path or config.default_path local current_dir = fs.get_current_dir() local abs_conf_dir, rel_conf_dir = fs.find_file(current_dir, path) if not abs_conf_dir then if is_default_path then if global_path and fs.is_file(global_path) then abs_conf_dir = current_dir rel_conf_dir = "" path = global_path else return config.empty_config end else return nil, "Couldn't find configuration file "..path end end local conf = { abs_dir = abs_conf_dir, rel_dir = rel_conf_dir, cur_dir = current_dir } local conf_path = fs.join(rel_conf_dir, path) local env, special_values = make_config_env() local loader = add_relative_loader(conf) local load_ok, ret, load_err = utils.load_config(conf_path, env) remove_relative_loader(loader) if not load_ok then return nil, ("Couldn't load configuration from %s: %s error (%s)"):format(conf_path, ret, load_err) end -- Support returning some options from config instead of setting them as globals. -- This allows easily loading options from another file, for example using require. if type(ret) == "table" then utils.update(env, ret) end remove_env_mt(env, special_values) -- Update stds before validating config - std validation relies on that. if type(env.stds) == "table" then -- Ideally config shouldn't mutate global builtin standards module, -- not if `luacheck.config` becomes public interface. utils.update(builtin_standards, env.stds) end local err = validate_config(env) if err then return nil, ("Couldn't load configuration from %s: %s"):format(conf_path, err) end conf.options = env conf.overrides = normalize_overrides(env.files, abs_conf_dir) return conf end -- Adjusts path starting from config dir to start from current directory. function config.relative_path(conf, path) if conf.empty then return path else return fs.join(conf.rel_dir, path) end end -- Requires module from config directory. -- Returns success flag and module or error message. function config.relative_require(conf, modname) local loader if not conf.empty then loader = add_relative_loader(conf) end local ok, mod_or_err = pcall(require, modname) if not conf.empty then remove_relative_loader(loader) end return ok, mod_or_err end -- Returns top-level options. function config.get_top_options(conf) return conf.empty and {} or conf.options end -- Returns array of options for a file. function config.get_options(conf, file) if conf.empty then return {} end local res = {conf.options} if type(file) ~= "string" then return res end local path = fs.normalize(fs.join(conf.cur_dir, file)) for _, override_glob in ipairs(conf.overrides.globs) do if globbing.match(override_glob, path) then table.insert(res, conf.overrides.options[override_glob]) end end return res end return config luacheck-0.21.1/src/luacheck/core_utils.lua000066400000000000000000000036471315524664100206140ustar00rootroot00000000000000local 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 visited[index] = true local item = line.items[index] if callback(line, index, item, ...) then return end if not item then return elseif item.tag == "Jump" then return core_utils.walk_line(line, visited, item.to, callback, ...) elseif item.tag == "Cjump" then core_utils.walk_line(line, visited, item.to, callback, ...) end 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 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) 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 else return event_priority(event1) < event_priority(event2) 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) end return core_utils luacheck-0.21.1/src/luacheck/detect_globals.lua000066400000000000000000000120561315524664100214110ustar00rootroot00000000000000local utils = require "luacheck.utils" 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 chstate:warn_global(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 -- Detects assignments, field accesses and mutations of global variables, -- tracing through localizing assignments such as `local t = table`. local function detect_globals(chstate, line) detect_in_line(chstate, line, true) for _, nested_line in ipairs(line.lines) do detect_in_line(chstate, nested_line) end end return detect_globals luacheck-0.21.1/src/luacheck/expand_rockspec.lua000066400000000000000000000023161315524664100216040ustar00rootroot00000000000000local utils = require "luacheck.utils" local function extract_lua_files(rockspec) if type(rockspec) ~= "table" then return nil, "rockspec is not a table" end local build = rockspec.build if type(build) ~= "table" then return nil, "rockspec.build is not a table" end local res = {} local function scan(t) if type(t) == "table" then for _, file in pairs(t) do if type(file) == "string" and file:sub(-#".lua") == ".lua" then table.insert(res, file) end end end end if build.type == "builtin" then scan(build.modules) end if type(build.install) == "table" then scan(build.install.lua) scan(build.install.bin) end table.sort(res) return res end -- Receives a name of a rockspec, returns list of related .lua files or nil and "syntax" or "error" and error message. local function expand_rockspec(file) local rockspec, err, msg = utils.load_config(file) 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 end return files end return expand_rockspec luacheck-0.21.1/src/luacheck/filter.lua000066400000000000000000000326361315524664100177310ustar00rootroot00000000000000local inline_options = require "luacheck.inline_options" local options = require "luacheck.options" local core_utils = require "luacheck.core_utils" 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 matches_code, matches_name local code_pattern, name_pattern = pattern[1], pattern[2] if code_pattern then matches_code = utils.pmatch(warning.code, code_pattern) end if name_pattern then if not warning.name then -- Warnings without name field can't match by name. matches_name = false else matches_name = utils.pmatch(warning.name, name_pattern) end end return matches_code, matches_name end local function is_enabled(rules, warning) -- A warning is enabled when its code and name are enabled. local enabled_code, enabled_name = false, false for _, rule in ipairs(rules) do local matches_one = false for _, pattern in ipairs(rule[1]) do local matches_code, matches_name = match(warning, pattern) -- If a factor is enabled, warning can't be disabled by it. if enabled_code then matches_code = rule[2] ~= "disable" end if enabled_name then matches_code = rule[2] ~= "disable" end if (matches_code and matches_name ~= false) or (matches_name and matches_code ~= false) then matches_one = true end if rule[2] == "enable" then if matches_code then enabled_code = true end if matches_name then enabled_name = true end if enabled_code and enabled_name then -- Enable as matching to some `enable` pattern by code and to another by name. return true end elseif rule[2] == "disable" then if matches_one then -- Disable as matching to `disable` pattern. return false end end end if rule[2] == "only" and not matches_one then -- Disable as not matching to any of `only` patterns. return false end end -- Enable by default. return true end 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 "?") end return table.concat(parts, ".") end local function get_field_status(opts, warning, depth) local def = opts.std local defined = true local read_only = true for i = 1, depth or #warning.indexing do local index_string = warning.indexing[i] if index_string == true then -- Indexing with something that may or may not be a string. if (def.fields and next(def.fields)) or def.other_fields then if def.deep_read_only then read_only = true else read_only = false end else defined = false end break elseif index_string == false then -- Indexing with not a string. if not def.other_fields then defined = false end break else -- Indexing with a constant string. if def.fields and def.fields[index_string] then -- The field is defined, recurse into it. def = def.fields[index_string] if def.read_only ~= nil then read_only = def.read_only end else -- The field is not defined, but it may be okay to index if `other_fields` is true. if not def.other_fields then defined = false end break end end end 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 local function filters(opts, warning) if warning.code == "631" then local max_line_length = get_max_line_length(opts, warning) if (not max_line_length or warning.end_column <= max_line_length) then return true end end if warning.code:match("[234]..") and warning.name == "_" and not warning.useless then return true end if warning.code:match("1[14].") and warning.indirect and get_field_status( opts, warning, warning.previous_indexing_len) == "undefined" then return true end if warning.code:match("1[14].") and not warning.module and get_field_status(opts, warning) ~= "undefined" then return true end if warning.secondary and not opts.unused_secondaries then return true end if warning.self and not opts.self then return true end return not is_enabled(opts.rules, warning) end local function filter_file_report(report) local res = {} for _, pair in ipairs(report) do local issue, opts = pair[1], pair[2] 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 if issue.code:match("11[23]") and get_field_status(opts, issue, 1) ~= "undefined" then issue.code = "14" .. issue.code:sub(3, 3) 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 if issue.code:match("1[24][23]") then issue.field = get_field_string(issue) end table.insert(res, issue) end end end return res 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 end end return res end -- 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) local opts = options.normalize(option_stack) if not opts.inline then local res = {} local issues = inline_options.get_issues(file_report.events) for i, issue in ipairs(issues) do res[i] = {issue, opts} end return res end local events, per_line_opts = inline_options.validate_options(file_report.events, file_report.per_line_options) local issues_with_inline_opts = inline_options.get_issues_and_affecting_options(events, per_line_opts) local normalized_options_cache = {} local res = {} for i, pair in ipairs(issues_with_inline_opts) do local issue, inline_opts = pair[1], pair[2] if not normalized_options_cache[inline_opts] then normalized_options_cache[inline_opts] = options.normalize(utils.concat_arrays({option_stack, inline_opts})) end res[i] = {issue, normalized_options_cache[inline_opts]} end return res end local function get_option_stack(opts, report_index) local res = {opts} if opts and opts[report_index] then res[2] = opts[report_index] for _, nested_opts in ipairs(opts[report_index]) do table.insert(res, nested_opts) end end return res end local function annotate_report_with_affecting_options(report, opts) local res = {} 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)) end end return res end local function add_long_line_warnings(report) local res = {} for i, file_report in ipairs(report) do if file_report.fatal then res[i] = file_report 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 }) end core_utils.sort_by_location(res[i].events) end end return res end -- Removes warnings from report that do not match options. -- `opts[i]`, if present, is used as options when processing `report[i]` -- together with options in its array part. function filter.filter(report, opts) report = add_long_line_warnings(report) report = annotate_report_with_affecting_options(report, opts) report = filter_implicit_defs(report) return filter_report(report) end return filter luacheck-0.21.1/src/luacheck/format.lua000066400000000000000000000316221315524664100177260ustar00rootroot00000000000000local 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", ["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] if type(message_format) == "function" then return message_format(warning) else return message_format end end local function plural(number) return (number == 1) and "" or "s" end local color_codes = { reset = 0, bright = 1, red = 31, green = 32 } local function encode_color(c) return "\27[" .. tostring(color_codes[c]) .. "m" end local function colorize(str, ...) str = str .. encode_color("reset") for _, color in ipairs({...}) do str = encode_color(color) .. str end return encode_color("reset") .. str end local function format_color(str, color, ...) return color and colorize(str, ...) or str end local function format_number(number, color) return format_color(tostring(number), color, "bright", (number > 0) and "red" or "reset") end -- Substitutes markers within string format with values from a table. -- "{field_name}" marker is replaced with `values.field_name`. -- "{field_name!}" marker adds highlight or quoting depending on color -- option. local function substitute(string_format, values, color) return (string_format:gsub("{([_a-zA-Z0-9]+)(!?)}", function(field_name, highlight) local value = tostring(assert(values[field_name], "No field " .. field_name)) if highlight == "!" then if color then return colorize(value, "bright") else return "'" .. value .. "'" end else return value end end)) end local function format_message(event, color) return substitute(get_message_format(event), event, color) end -- Returns formatted message for an issue, without color. function format.get_message(event) return format_message(event) end local function capitalize(str) return str:gsub("^.", string.upper) end local function fatal_type(file_report) return capitalize(file_report.fatal) .. " error" end local function count_warnings_errors(events) local warnings, errors = 0, 0 for _, event in ipairs(events) do if event.code:sub(1, 1) == "0" then errors = errors + 1 else warnings = warnings + 1 end end return warnings, errors end local function format_file_report_header(report, file_name, opts) local label = "Checking " .. file_name local status if report.fatal then status = format_color(fatal_type(report), opts.color, "bright") elseif #report == 0 then status = format_color("OK", opts.color, "bright", "green") else local warnings, errors = count_warnings_errors(report) if warnings > 0 then status = format_color(tostring(warnings).." warning"..plural(warnings), opts.color, "bright", "red") end if errors > 0 then status = status and (status.." / ") or "" status = status..(format_color(tostring(errors).." error"..plural(errors), opts.color, "bright")) end end return label .. (" "):rep(math.max(50 - #label, 1)) .. status end local function format_location(file, location, opts) local res = ("%s:%d:%d"):format(file, location.line, location.column) if opts.ranges then res = ("%s-%d"):format(res, location.end_column) end return res end local function event_code(event) return (event.code:sub(1, 1) == "0" and "E" or "W")..event.code end local function format_event(file_name, event, opts) local message = format_message(event, opts.color) if opts.codes then message = ("(%s) %s"):format(event_code(event), message) end return format_location(file_name, event, opts) .. ": " .. message end local function format_file_report(report, file_name, opts) local buf = {format_file_report_header(report, file_name, opts)} if #report > 0 then table.insert(buf, "") for _, event in ipairs(report) do table.insert(buf, " " .. format_event(file_name, event, opts)) end table.insert(buf, "") elseif report.fatal then table.insert(buf, "") table.insert(buf, " " .. file_name .. ": " .. report.msg) end return table.concat(buf, "\n") end local function escape_xml(str) str = str:gsub("&", "&") str = str:gsub('"', """) str = str:gsub("'", "'") str = str:gsub("<", "<") str = str:gsub(">", ">") return str end local formatters = {} function formatters.default(report, file_names, opts) local buf = {} if opts.quiet <= 2 then for i, file_report in ipairs(report) do if opts.quiet == 0 or file_report.fatal or #file_report > 0 then table.insert(buf, (opts.quiet == 2 and format_file_report_header or format_file_report) ( file_report, file_names[i], opts)) end end if #buf > 0 and buf[#buf]:sub(-1) ~= "\n" then table.insert(buf, "") end end local total = ("Total: %s warning%s / %s error%s in %d file%s"):format( format_number(report.warnings, opts.color), plural(report.warnings), format_number(report.errors, opts.color), plural(report.errors), #report - report.fatals, plural(#report - report.fatals)) if report.fatals > 0 then total = total..(", couldn't check %s file%s"):format( report.fatals, plural(report.fatals)) end table.insert(buf, total) return table.concat(buf, "\n") end function formatters.TAP(report, file_names, opts) opts.color = false local buf = {} for i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ("not ok %d %s: %s"):format(#buf + 1, file_names[i], fatal_type(file_report))) elseif #file_report == 0 then table.insert(buf, ("ok %d %s"):format(#buf + 1, file_names[i])) else for _, warning in ipairs(file_report) do table.insert(buf, ("not ok %d %s"):format(#buf + 1, format_event(file_names[i], warning, opts))) end end end table.insert(buf, 1, "1.." .. tostring(#buf)) return table.concat(buf, "\n") end function formatters.JUnit(report, file_names) -- JUnit formatter doesn't support any options. local opts = {} local buf = {[[]]} local num_testcases = 0 for _, file_report in ipairs(report) do if file_report.fatal or #file_report == 0 then num_testcases = num_testcases + 1 else num_testcases = num_testcases + #file_report end end table.insert(buf, ([[]]):format(num_testcases)) for file_i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ([[ ]]):format( escape_xml(file_names[file_i]), escape_xml(file_names[file_i]))) table.insert(buf, ([[ ]]):format(escape_xml(fatal_type(file_report)))) table.insert(buf, [[ ]]) elseif #file_report == 0 then table.insert(buf, ([[ ]]):format( escape_xml(file_names[file_i]), escape_xml(file_names[file_i]))) else for event_i, event in ipairs(file_report) do table.insert(buf, ([[ ]]):format( escape_xml(file_names[file_i]), event_i, escape_xml(file_names[file_i]))) table.insert(buf, ([[ ]]):format( escape_xml(event_code(event)), escape_xml(format_event(file_names[file_i], event, opts)))) table.insert(buf, [[ ]]) end end end table.insert(buf, [[]]) return table.concat(buf, "\n") end function formatters.plain(report, file_names, opts) opts.color = false local buf = {} for i, file_report in ipairs(report) do if file_report.fatal then table.insert(buf, ("%s: %s (%s)"):format(file_names[i], fatal_type(file_report), file_report.msg)) else for _, event in ipairs(file_report) do table.insert(buf, format_event(file_names[i], event, opts)) end end end return table.concat(buf, "\n") end --- Formats a report. -- Recognized options: -- `options.formatter`: name of used formatter. Default: "default". -- `options.quiet`: integer in range 0-3. See CLI. Default: 0. -- `options.color`: should use ansicolors? Default: true. -- `options.codes`: should output warning codes? Default: false. -- `options.ranges`: should output token end column? Default: false. function format.format(report, file_names, options) return formatters[options.formatter or "default"](report, file_names, { quiet = options.quiet or 0, color = (options.color ~= false) and color_support, codes = options.codes, ranges = options.ranges }) end return format luacheck-0.21.1/src/luacheck/fs.lua000066400000000000000000000103031315524664100170370ustar00rootroot00000000000000local fs = {} 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 end return path end function fs.split_base(path) if utils.is_windows then if path:match("^%a:\\") then return path:sub(1, 3), path:sub(4) else -- Disregard UNC paths and relative paths with drive letter. return "", path end else if path:match("^/") then if path:match("^//") then return "//", path:sub(3) else return "/", path:sub(2) end else return "", path end end end local function is_absolute(path) return fs.split_base(path) ~= "" end function fs.normalize(path) local base, rest = fs.split_base(path) rest = rest:gsub("[/\\]", utils.dir_sep) local parts = {} for part in rest:gmatch("[^"..utils.dir_sep.."]+") do if part ~= "." then if part == ".." and #parts > 0 and parts[#parts] ~= ".." then parts[#parts] = nil else parts[#parts + 1] = part end end end if base == "" and #parts == 0 then return "." else return base..table.concat(parts, utils.dir_sep) end end local function join_two_paths(base, path) if base == "" or is_absolute(path) then return path else return ensure_dir_sep(base) .. path end end function fs.join(base, ...) local res = base for i = 1, select("#", ...) do res = join_two_paths(res, select(i, ...)) end return res end function fs.is_subpath(path, subpath) local base1, rest1 = fs.split_base(path) local base2, rest2 = fs.split_base(subpath) if base1 ~= base2 then return false end if rest2:sub(1, #rest1) ~= rest1 then return false end return rest1 == rest2 or rest2:sub(#rest1 + 1, #rest1 + 1) == utils.dir_sep end function fs.is_dir(path) return base_fs.get_mode(path) == "directory" end function fs.is_file(path) return base_fs.get_mode(path) == "file" end -- Searches for file starting from path, going up until the file -- is found or root directory is reached. -- Path must be absolute. -- Returns absolute and relative paths to directory containing file or nil. function fs.find_file(path, file) if is_absolute(file) then return fs.is_file(file) and path, "" end path = fs.normalize(path) local base, rest = fs.split_base(path) local rel_path = "" while true do if fs.is_file(fs.join(base..rest, file)) then return base..rest, rel_path elseif rest == "" then break end rest = rest:match("^(.*)"..utils.dir_sep..".*$") or "" rel_path = rel_path..".."..utils.dir_sep end end -- Returns list of all files in directory matching pattern. -- Returns nil, error message on error. function fs.extract_files(dir_path, pattern) assert(fs.has_lfs) local res = {} local function scan(dir) local ok, iter, state, var = pcall(base_fs.dir_iter, dir) if not ok then local err = utils.unprefix(iter, "cannot open " .. dir .. ": ") return "couldn't recursively check " .. dir .. ": " .. err end for path in iter, state, var do if path ~= "." and path ~= ".." then local full_path = fs.join(dir, path) if fs.is_dir(full_path) then local err = scan(full_path) if err then return err end elseif path:match(pattern) and fs.is_file(full_path) then table.insert(res, full_path) end end end end local err = scan(dir_path) if err then return nil, err end table.sort(res) return res end -- Returns modification time for a file. function fs.get_mtime(path) assert(fs.has_lfs) return base_fs.get_mtime(path) 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()) end return fs luacheck-0.21.1/src/luacheck/globbing.lua000066400000000000000000000102531315524664100202160ustar00rootroot00000000000000local fs = require "luacheck.fs" local utils = require "luacheck.utils" -- Only ?, *, ** and simple character classes (with ranges and negation) are supported. -- Hidden files are not treated specially. Special characters can't be escaped. local globbing = {} local cur_dir = fs.get_current_dir() local function is_regular_path(glob) return not glob:find("[*?%[]") end local function get_parts(path) local parts = {} for part in path:gmatch("[^"..utils.dir_sep.."]+") do table.insert(parts, part) end return parts end local function glob_pattern_escaper(c) return ((c == "*" or c == "?") and "." or "%")..c end local function glob_range_escaper(c) return c == "-" and c or ("%"..c) end local function glob_part_to_pattern(glob_part) local buffer = {"^"} local i = 1 while i <= #glob_part do local bracketless bracketless, i = glob_part:match("([^%[]*)()", i) table.insert(buffer, (bracketless:gsub("%p", glob_pattern_escaper))) if glob_part:sub(i, i) == "[" then table.insert(buffer, "[") i = i + 1 local first_char = glob_part:sub(i, i) if first_char == "!" then table.insert(buffer, "^") i = i + 1 elseif first_char == "]" then table.insert(buffer, "%]") i = i + 1 end bracketless, i = glob_part:match("([^%]]*)()", i) if bracketless:sub(1, 1) == "-" then table.insert(buffer, "%-") bracketless = bracketless:sub(2) end local last_dash = "" if bracketless:sub(-1) == "-" then last_dash = "-" bracketless = bracketless:sub(1, -2) end table.insert(buffer, (bracketless:gsub("%p", glob_range_escaper))) table.insert(buffer, last_dash.."]") i = i + 1 end end table.insert(buffer, "$") return table.concat(buffer) end local function part_match(glob_part, path_part) return utils.pmatch(path_part, glob_part_to_pattern(glob_part)) end local function parts_match(glob_parts, glob_i, path_parts, path_i) local glob_part = glob_parts[glob_i] if not glob_part then -- Reached glob end, path matches the glob or its subdirectory. -- E.g. path "foo/bar/baz/src.lua" matches glob "foo/*/baz". return true end if glob_part == "**" then -- "**" can consume any number of path parts. for i = path_i, #path_parts + 1 do if parts_match(glob_parts, glob_i + 1, path_parts, i) then return true end end return false end local path_part = path_parts[path_i] return path_part and part_match(glob_part, path_part) and ( parts_match(glob_parts, glob_i + 1, path_parts, path_i + 1)) end -- Checks if a path matches a globbing pattern. function globbing.match(glob, path) glob = fs.normalize(fs.join(cur_dir, glob)) path = fs.normalize(fs.join(cur_dir, path)) if is_regular_path(glob) then return fs.is_subpath(glob, path) end local glob_base, path_base glob_base, glob = fs.split_base(glob) path_base, path = fs.split_base(path) if glob_base ~= path_base then return false end local glob_parts = get_parts(glob) local path_parts = get_parts(path) return parts_match(glob_parts, 1, path_parts, 1) end -- Checks if glob1 is less specific than glob2 and should be applied -- first in overrides. function globbing.compare(glob1, glob2) local base1, base2 base1, glob1 = fs.split_base(glob1) base2, glob2 = fs.split_base(glob2) if base1 ~= base2 then return base1 < base2 end local parts1 = get_parts(glob1) local parts2 = get_parts(glob2) for i = 1, math.max(#parts1, #parts2) do if not parts1[i] then return true elseif not parts2[i] then return false end if (parts1[i] == "**" or parts2[i] == "**") and parts1[i] ~= parts2[i] then return parts1[i] == "**" end local _, specials1 = parts1[i]:gsub("[%*%?%[]", {}) local _, specials2 = parts2[i]:gsub("[%*%?%[]", {}) if specials1 ~= specials2 then return specials1 > specials2 end end return glob1 < glob2 end return globbing luacheck-0.21.1/src/luacheck/init.lua000066400000000000000000000103421315524664100173750ustar00rootroot00000000000000local check = require "luacheck.check" local filter = require "luacheck.filter" local options = require "luacheck.options" local format = require "luacheck.format" local utils = require "luacheck.utils" local luacheck = { _VERSION = "0.21.1" } local function raw_validate_options(fname, opts) assert(opts == nil or type(opts) == "table", ("bad argument #2 to '%s' (table or nil expected, got %s)"):format(fname, type(opts)) ) local ok, invalid_field = options.validate(options.all_options, opts) if not ok then if invalid_field then error(("bad argument #2 to '%s' (invalid value of option '%s')"):format(fname, invalid_field)) else error(("bad argument #2 to '%s'"):format(fname)) end end end local function validate_options(fname, items, opts) raw_validate_options(fname, opts) if opts ~= nil then for i in ipairs(items) do raw_validate_options(fname, opts[i]) if opts[i] ~= nil then for _, nested_opts in ipairs(opts[i]) do raw_validate_options(fname, nested_opts) end end end end end -- Returns report for a string. function luacheck.get_report(src) local msg = ("bad argument #1 to 'luacheck.get_report' (string expected, got %s)"):format(type(src)) assert(type(src) == "string", msg) return check(src) end -- Applies options to reports. Reports with .fatal field are unchanged. -- Options are applied to reports[i] in order: options, options[i], options[i][1], options[i][2], ... -- Returns new array of reports, adds .warnings, .errors and .fatals fields to this array. function luacheck.process_reports(reports, opts) local msg = ("bad argument #1 to 'luacheck.process_reports' (table expected, got %s)"):format(type(reports)) assert(type(reports) == "table", msg) validate_options("luacheck.process_reports", reports, opts) local report = filter.filter(reports, opts) report.warnings = 0 report.errors = 0 report.fatals = 0 for _, file_report in ipairs(report) do if file_report.fatal then report.fatals = report.fatals + 1 else for _, event in ipairs(file_report) do if event.code:sub(1, 1) == "0" then report.errors = report.errors + 1 else report.warnings = report.warnings + 1 end end end end return report end -- Checks strings with options, returns report. -- Tables with .fatal field are unchanged. function luacheck.check_strings(srcs, opts) local msg = ("bad argument #1 to 'luacheck.check_strings' (table expected, got %s)"):format(type(srcs)) assert(type(srcs) == "table", msg) for _, item in ipairs(srcs) do msg = ("bad argument #1 to 'luacheck.check_strings' (array of strings or tables expected, got %s)"):format( type(item)) assert(type(item) == "string" or type(item) == "table", msg) end validate_options("luacheck.check_strings", srcs, opts) local reports = {} for i, src in ipairs(srcs) do if type(src) == "table" and src.fatal then reports[i] = src else reports[i] = luacheck.get_report(src) end end return luacheck.process_reports(reports, opts) end function luacheck.check_files(files, opts) local msg = ("bad argument #1 to 'luacheck.check_files' (table expected, got %s)"):format(type(files)) assert(type(files) == "table", msg) for _, item in ipairs(files) do msg = ("bad argument #1 to 'luacheck.check_files' (array of paths or file handles expected, got %s)"):format( type(item)) assert(type(item) == "string" or io.type(item) == "file", msg ) end validate_options("luacheck.check_files", files, opts) local srcs = {} for i, file in ipairs(files) do local src, err = utils.read_file(file) srcs[i] = src or {fatal = "I/O", msg = err} end return luacheck.check_strings(srcs, opts) end function luacheck.get_message(issue) local msg = ("bad argument #1 to 'luacheck.get_message' (table expected, got %s)"):format(type(issue)) assert(type(issue) == "table", msg) return format.get_message(issue) end setmetatable(luacheck, {__call = function(_, ...) return luacheck.check_files(...) end}) return luacheck luacheck-0.21.1/src/luacheck/inline_options.lua000066400000000000000000000332371315524664100214730ustar00rootroot00000000000000local 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 max_line_length_opts = utils.array_to_set({ "max_line_length", "max_code_line_length", "max_string_line_length", "max_comment_line_length"}) 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 max_line_length_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(max_line_length_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(ast, comments, code_lines, warnings) local events = utils.update({}, warnings) add_closure_boundaries(ast, events) local per_line_opts = add_inline_options(events, comments, 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 local function invalid_option_value(invalid_opt) return ("invalid value of inline option '%s'"):format(invalid_opt) 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 require 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) local new_events = {} local new_per_line_opts = {} local added_errors = false for i, event in ipairs(events) do if event.options then local valid, invalid_opt = options.validate(options.all_options, event.options) if valid then new_events[i] = event else new_events[i] = invalid_options_error(event, invalid_option_value(invalid_opt)) 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 valid, invalid_opt = options.validate(options.all_options, event.options) if valid 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, invalid_option_value(invalid_opt))) 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 luacheck-0.21.1/src/luacheck/lexer.lua000066400000000000000000000442611315524664100175600ustar00rootroot00000000000000local utils = require "luacheck.utils" -- Lexer should support syntax of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT(64bit and complex cdata literals). local lexer = {} local sbyte = string.byte local ssub = string.sub local schar = string.char local sreverse = string.reverse local tconcat = table.concat local mfloor = math.floor -- No point in inlining these, fetching a constant ~= fetching a local. local BYTE_0, BYTE_9, BYTE_f, BYTE_F = sbyte("0"), sbyte("9"), sbyte("f"), sbyte("F") local BYTE_x, BYTE_X, BYTE_i, BYTE_I = sbyte("x"), sbyte("X"), sbyte("i"), sbyte("I") local BYTE_l, BYTE_L, BYTE_u, BYTE_U = sbyte("l"), sbyte("L"), sbyte("u"), sbyte("U") local BYTE_e, BYTE_E, BYTE_p, BYTE_P = sbyte("e"), sbyte("E"), sbyte("p"), sbyte("P") local BYTE_a, BYTE_z, BYTE_A, BYTE_Z = sbyte("a"), sbyte("z"), sbyte("A"), sbyte("Z") local BYTE_DOT, BYTE_COLON = sbyte("."), sbyte(":") local BYTE_OBRACK, BYTE_CBRACK = sbyte("["), sbyte("]") local BYTE_OBRACE, BYTE_CBRACE = sbyte("{"), sbyte("}") local BYTE_QUOTE, BYTE_DQUOTE = sbyte("'"), sbyte('"') local BYTE_PLUS, BYTE_DASH, BYTE_LDASH = sbyte("+"), sbyte("-"), sbyte("_") local BYTE_SLASH, BYTE_BSLASH = sbyte("/"), sbyte("\\") local BYTE_EQ, BYTE_NE = sbyte("="), sbyte("~") local BYTE_LT, BYTE_GT = sbyte("<"), sbyte(">") local BYTE_LF, BYTE_CR = sbyte("\n"), sbyte("\r") local BYTE_SPACE, BYTE_FF, BYTE_TAB, BYTE_VTAB = sbyte(" "), sbyte("\f"), sbyte("\t"), sbyte("\v") local function to_hex(b) if BYTE_0 <= b and b <= BYTE_9 then return b-BYTE_0 elseif BYTE_a <= b and b <= BYTE_f then return 10+b-BYTE_a elseif BYTE_A <= b and b <= BYTE_F then return 10+b-BYTE_A else return nil end end local function to_dec(b) if BYTE_0 <= b and b <= BYTE_9 then return b-BYTE_0 else return nil end end local function to_utf(codepoint) if codepoint < 0x80 then -- ASCII? return schar(codepoint) end local buf = {} local mfb = 0x3F repeat buf[#buf+1] = schar(codepoint % 0x40 + 0x80) codepoint = mfloor(codepoint / 0x40) mfb = mfloor(mfb / 2) until codepoint <= mfb buf[#buf+1] = schar(0xFE - mfb*2 + codepoint) return sreverse(tconcat(buf)) end local function is_alpha(b) return (BYTE_a <= b and b <= BYTE_z) or (BYTE_A <= b and b <= BYTE_Z) or b == BYTE_LDASH end local function is_newline(b) return (b == BYTE_LF) or (b == BYTE_CR) end local function is_space(b) return (b == BYTE_SPACE) or (b == BYTE_FF) or (b == BYTE_TAB) or (b == BYTE_VTAB) end local keywords = utils.array_to_set({ "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}) local simple_escapes = { [sbyte("a")] = sbyte("\a"), [sbyte("b")] = sbyte("\b"), [sbyte("f")] = sbyte("\f"), [sbyte("n")] = sbyte("\n"), [sbyte("r")] = sbyte("\r"), [sbyte("t")] = sbyte("\t"), [sbyte("v")] = sbyte("\v"), [BYTE_BSLASH] = BYTE_BSLASH, [BYTE_QUOTE] = BYTE_QUOTE, [BYTE_DQUOTE] = BYTE_DQUOTE } local function next_byte(state, inc) inc = inc or 1 state.offset = state.offset+inc return sbyte(state.src, state.offset) end -- Skipping helpers. -- Take the current character, skip something, return next character. local function skip_newline(state, newline) local b = next_byte(state) if b ~= newline and is_newline(b) then b = next_byte(state) end state.line = state.line+1 state.line_offset = state.offset return b end local function skip_till_newline(state, b) while not is_newline(b) and b ~= nil do b = next_byte(state) end return b end local function skip_space(state, b) while is_space(b) or is_newline(b) do if is_newline(b) then b = skip_newline(state, b) else b = next_byte(state) end end return b end -- Skips "[=*" or "]=*". Returns next character and number of "="s. local function skip_long_bracket(state) local start = state.offset local b = next_byte(state) while b == BYTE_EQ do b = next_byte(state) end return b, state.offset-start-1 end -- Token handlers. -- Called after the opening "[=*" has been skipped. -- Takes number of "=" in the opening bracket and token type(comment or string). local function lex_long_string(state, opening_long_bracket, token) local b = next_byte(state) if is_newline(b) then b = skip_newline(state, b) end local lines = {} local line_start = state.offset while true do if is_newline(b) then -- Add the finished line. lines[#lines+1] = ssub(state.src, line_start, state.offset-1) b = skip_newline(state, b) line_start = state.offset elseif b == BYTE_CBRACK then local long_bracket b, long_bracket = skip_long_bracket(state) if b == BYTE_CBRACK and long_bracket == opening_long_bracket then break end elseif b == nil then return nil, token == "string" and "unfinished long string" or "unfinished long comment" else b = next_byte(state) end end -- Add last line. lines[#lines+1] = ssub(state.src, line_start, state.offset-opening_long_bracket-2) next_byte(state) return token, tconcat(lines, "\n") end local function lex_short_string(state, quote) local b = next_byte(state) local chunks -- Buffer is only required when there are escape sequences. local chunk_start = state.offset while b ~= quote do if b == BYTE_BSLASH then -- Escape sequence. if not chunks then -- This is the first escape sequence, init buffer. chunks = {} end -- Put previous chunk into buffer. if chunk_start ~= state.offset then chunks[#chunks+1] = ssub(state.src, chunk_start, state.offset-1) end b = next_byte(state) -- The final string escape sequence evaluates to. local s local escape_byte = simple_escapes[b] if escape_byte then -- Is it a simple escape sequence? b = next_byte(state) s = schar(escape_byte) elseif is_newline(b) then b = skip_newline(state, b) s = "\n" elseif b == BYTE_x then -- Hexadecimal escape. b = next_byte(state) -- Skip "x". -- Exactly two hexadecimal digits. local c1, c2 if b then c1 = to_hex(b) end if not c1 then return nil, "invalid hexadecimal escape sequence", -2 end b = next_byte(state) if b then c2 = to_hex(b) end if not c2 then return nil, "invalid hexadecimal escape sequence", -3 end b = next_byte(state) s = schar(c1*16 + c2) elseif b == BYTE_u then b = next_byte(state) -- Skip "u". if b ~= BYTE_OBRACE then return nil, "invalid UTF-8 escape sequence", -2 end b = next_byte(state) -- Skip "{". local codepoint -- There should be at least one digit. if b then codepoint = to_hex(b) end if not codepoint then return nil, "invalid UTF-8 escape sequence", -3 end local hexdigits = 0 while true do b = next_byte(state) local hex if b then hex = to_hex(b) end if hex then hexdigits = hexdigits + 1 codepoint = codepoint*16 + hex if codepoint > 0x10FFFF then -- UTF-8 value too large. return nil, "invalid UTF-8 escape sequence", -hexdigits-3 end else break end end if b ~= BYTE_CBRACE then return nil, "invalid UTF-8 escape sequence", -hexdigits-4 end b = next_byte(state) -- Skip "}". s = to_utf(codepoint) elseif b == BYTE_z then -- Zap following span of spaces. b = skip_space(state, next_byte(state)) else -- Must be a decimal escape. local cb if b then cb = to_dec(b) end if not cb then return nil, "invalid escape sequence", -1 end -- Up to three decimal digits. b = next_byte(state) if b then local c2 = to_dec(b) if c2 then cb = 10*cb + c2 b = next_byte(state) if b then local c3 = to_dec(b) if c3 then cb = 10*cb + c3 if cb > 255 then return nil, "invalid decimal escape sequence", -3 end b = next_byte(state) end end end end s = schar(cb) end if s then chunks[#chunks+1] = s end -- Next chunk starts after escape sequence. chunk_start = state.offset elseif b == nil or is_newline(b) then return nil, "unfinished string" else b = next_byte(state) end end -- Offset now points at the closing quote. local string_value if chunks then -- Put last chunk into buffer. if chunk_start ~= state.offset then chunks[#chunks+1] = ssub(state.src, chunk_start, state.offset-1) end string_value = tconcat(chunks) else -- There were no escape sequences. string_value = ssub(state.src, chunk_start, state.offset-1) end next_byte(state) -- Skip the closing quote. return "string", string_value end -- Payload for a number is simply a substring. -- Luacheck is supposed to be forward-compatible with Lua 5.3 and LuaJIT syntax, so -- parsing it into actual number may be problematic. -- It is not needed currently anyway as Luacheck does not do static evaluation yet. local function lex_number(state, b) local start = state.offset local exp_lower, exp_upper = BYTE_e, BYTE_E local is_digit = to_dec local has_digits = false local is_float = false if b == BYTE_0 then b = next_byte(state) if b == BYTE_x or b == BYTE_X then exp_lower, exp_upper = BYTE_p, BYTE_P is_digit = to_hex b = next_byte(state) else has_digits = true end end while b ~= nil and is_digit(b) do b = next_byte(state) has_digits = true end if b == BYTE_DOT then -- Fractional part. is_float = true b = next_byte(state) -- Skip dot. while b ~= nil and is_digit(b) do b = next_byte(state) has_digits = true end end if b == exp_lower or b == exp_upper then -- Exponent part. is_float = true b = next_byte(state) -- Skip optional sign. if b == BYTE_PLUS or b == BYTE_DASH then b = next_byte(state) end -- Exponent consists of one or more decimal digits. if b == nil or not to_dec(b) then return nil, "malformed number" end repeat b = next_byte(state) until b == nil or not to_dec(b) end if not has_digits then return nil, "malformed number" end -- Is it cdata literal? if b == BYTE_i or b == BYTE_I then -- It is complex literal. Skip "i" or "I". next_byte(state) else -- uint64_t and int64_t literals can not be fractional. if not is_float then if b == BYTE_u or b == BYTE_U then -- It may be uint64_t literal. local b1, b2 = sbyte(state.src, state.offset+1, state.offset+2) if (b1 == BYTE_l or b1 == BYTE_L) and (b2 == BYTE_l or b2 == BYTE_L) then -- It is uint64_t literal. next_byte(state, 3) end elseif b == BYTE_l or b == BYTE_L then -- It may be uint64_t or int64_t literal. local b1, b2 = sbyte(state.src, state.offset+1, state.offset+2) if b1 == BYTE_l or b1 == BYTE_L then if b2 == BYTE_u or b2 == BYTE_U then -- It is uint64_t literal. next_byte(state, 3) else -- It is int64_t literal. next_byte(state, 2) end end end end end return "number", ssub(state.src, start, state.offset-1) end local function lex_ident(state) local start = state.offset local b = next_byte(state) while (b ~= nil) and (is_alpha(b) or to_dec(b)) do b = next_byte(state) end local ident = ssub(state.src, start, state.offset-1) if keywords[ident] then return ident else return "name", ident end end local function lex_dash(state) local b = next_byte(state) -- Is it "-" or comment? if b ~= BYTE_DASH then return "-" else -- It is a comment. b = next_byte(state) local start = state.offset -- Is it a long comment? if b == BYTE_OBRACK then local long_bracket b, long_bracket = skip_long_bracket(state) if b == BYTE_OBRACK then return lex_long_string(state, long_bracket, "comment") end end -- Short comment. b = skip_till_newline(state, b) local comment_value = ssub(state.src, start, state.offset-1) skip_newline(state, b) return "comment", comment_value end end local function lex_bracket(state) -- Is it "[" or long string? local b, long_bracket = skip_long_bracket(state) if b == BYTE_OBRACK then return lex_long_string(state, long_bracket, "string") elseif long_bracket == 0 then return "[" else return nil, "invalid long string delimiter" end end local function lex_eq(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "==" else return "=" end end local function lex_lt(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "<=" elseif b == BYTE_LT then next_byte(state) return "<<" else return "<" end end local function lex_gt(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return ">=" elseif b == BYTE_GT then next_byte(state) return ">>" else return ">" end end local function lex_div(state) local b = next_byte(state) if b == BYTE_SLASH then next_byte(state) return "//" else return "/" end end local function lex_ne(state) local b = next_byte(state) if b == BYTE_EQ then next_byte(state) return "~=" else return "~" end end local function lex_colon(state) local b = next_byte(state) if b == BYTE_COLON then next_byte(state) return "::" else return ":" end end local function lex_dot(state) local b = next_byte(state) if b == BYTE_DOT then b = next_byte(state) if b == BYTE_DOT then next_byte(state) return "...", "..." else return ".." end elseif b and to_dec(b) then -- Backtrack to dot. return lex_number(state, next_byte(state, -1)) else return "." end end local function lex_any(state, b) next_byte(state) return schar(b) end -- Maps first bytes of tokens to functions that handle them. -- Each handler takes the first byte as an argument. -- Each handler stops at the character after the token and returns the token and, -- optionally, a value associated with the token. -- On error handler returns nil, error message and, optionally, start of reported location as negative offset. local byte_handlers = { [BYTE_DOT] = lex_dot, [BYTE_COLON] = lex_colon, [BYTE_OBRACK] = lex_bracket, [BYTE_QUOTE] = lex_short_string, [BYTE_DQUOTE] = lex_short_string, [BYTE_DASH] = lex_dash, [BYTE_SLASH] = lex_div, [BYTE_EQ] = lex_eq, [BYTE_NE] = lex_ne, [BYTE_LT] = lex_lt, [BYTE_GT] = lex_gt, [BYTE_LDASH] = lex_ident } for b=BYTE_0, BYTE_9 do byte_handlers[b] = lex_number end for b=BYTE_a, BYTE_z do byte_handlers[b] = lex_ident end for b=BYTE_A, BYTE_Z do byte_handlers[b] = lex_ident end local function decimal_escaper(char) return "\\" .. tostring(sbyte(char)) end -- Returns quoted printable representation of s. function lexer.quote(s) return "'" .. s:gsub("[^\32-\126]", decimal_escaper) .. "'" end -- Creates and returns lexer state for source. function lexer.new_state(src) local state = { src = src, line = 1, line_offset = 1, offset = 1 } if ssub(src, 1, 2) == "#!" then -- Skip shebang. skip_newline(state, skip_till_newline(state, next_byte(state, 2))) end return state end -- Looks for next token starting from state.line, state.line_offset, state.offset. -- Returns next token, its value and its location (line, column, offset). -- Sets state.line, state.line_offset, state.offset to token end location + 1. -- On error returns nil, error message, error location (line, column, offset), error end column. function lexer.next_token(state) local b = skip_space(state, sbyte(state.src, state.offset)) -- Save location of token start. local token_line = state.line local token_column = state.offset - state.line_offset + 1 local token_offset = state.offset local token, token_value, err_offset, err_end_column if b == nil then token = "eof" else token, token_value, err_offset = (byte_handlers[b] or lex_any)(state, b) end if err_offset then local token_body = ssub(state.src, state.offset + err_offset, state.offset) token_value = token_value .. " " .. lexer.quote(token_body) token_line = state.line token_column = state.offset - state.line_offset + 1 + err_offset token_offset = state.offset + err_offset err_end_column = token_column + #token_body - 1 end return token, token_value, token_line, token_column, token_offset, err_end_column or token_column end return lexer luacheck-0.21.1/src/luacheck/lfs_fs.lua000066400000000000000000000005411315524664100177060ustar00rootroot00000000000000local 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 function lfs_fs.dir_iter(path) return lfs.dir(path) end return lfs_fs luacheck-0.21.1/src/luacheck/linearize.lua000066400000000000000000000424341315524664100204230ustar00rootroot00000000000000local parser = require "luacheck.parser" local utils = require "luacheck.utils" local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"}) -- Who needs classes anyway. local function new_line(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 self.chstate:warn_unused_label(label) end end for _, var in pairs(left_scope.vars) do var.scope_end = self.lines.top.items.size end end function LinState:register_var(node, type_) local var = new_var(self.lines.top, node, type_) local prev_var = self:resolve_var(var.name) if prev_var then local same_scope = self.scopes.top.vars[var.name] self.chstate:warn_redefined(var, prev_var, same_scope) if same_scope then prev_var.scope_end = self.lines.top.items.size end end self.scopes.top.vars[var.name] = var node.var = var return var end function LinState:register_vars(nodes, type_) for _, node in ipairs(nodes) do self:register_var(node, type_) end end function LinState:resolve_var(name) for _, scope in utils.ripairs(self.scopes) do local var = scope.vars[name] if var then return var end end end function LinState:check_var(node) 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) if self.scopes.top.labels[name] then assert(not pseudo_labels[name]) parser.syntax_error(location, end_column, ("label '%s' already defined on line %d"):format( name, self.scopes.top.labels[name].location.line)) end self.scopes.top.labels[name] = new_label(self.lines.top, name, location, end_column) end -- `node` is assignment node (`Local or `Set). function LinState:check_balance(node) if node[2] then if #node[1] < #node[2] then self.chstate:warn_unbalanced(node.equals_location, true) elseif (#node[1] > #node[2]) and node.tag ~= "Local" and not is_unpacking(node[2][#node[2]]) then self.chstate:warn_unbalanced(node.equals_location) end end end function LinState:check_empty_block(block) if #block == 0 then self.chstate:warn_empty_block(block.location, block.tag == "Do") end end function LinState:emit(item) self.lines.top.items:push(item) end function LinState:emit_goto(name, is_conditional, location) local jump = new_jump_item(is_conditional) self:emit(jump) table.insert(self.scopes.top.gotos, new_goto(name, jump, location)) end local tag_to_boolean = { Nil = false, False = false, True = true, Number = true, String = true, Table = true, Function = true } -- Emits goto that jumps to ::name:: if bool(cond_node) == false. function LinState:emit_cond_goto(name, cond_node) local cond_bool = tag_to_boolean[cond_node.tag] if cond_bool ~= true then self:emit_goto(name, cond_bool ~= false) end end function LinState:emit_noop(node, loop_end) self:emit(new_noop_item(node, loop_end)) end function LinState:emit_stmt(stmt) self["emit_stmt_" .. stmt.tag](self, stmt) end function LinState:emit_stmts(stmts) for _, stmt in ipairs(stmts) do self:emit_stmt(stmt) end end function LinState:emit_block(block) self:enter_scope() self:emit_stmts(block) self:leave_scope() end function LinState:emit_stmt_Do(node) self:check_empty_block(node) self:emit_noop(node) self:emit_block(node) end function LinState:emit_stmt_While(node) self:emit_noop(node) self:enter_scope() self:register_label("do") self:emit_expr(node[1]) self:emit_cond_goto("break", node[1]) self:emit_block(node[2]) self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Repeat(node) self:emit_noop(node) self:enter_scope() self:register_label("do") self:enter_scope() self:emit_stmts(node[1]) self:emit_expr(node[2]) self:leave_scope() self:emit_cond_goto("do", node[2]) self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Fornum(node) self:emit_noop(node) self:emit_expr(node[2]) self:emit_expr(node[3]) if node[5] then self:emit_expr(node[4]) end self:enter_scope() self:register_label("do") self:emit_goto("break", true) self:enter_scope() self:emit(new_local_item({node[1]})) self:register_var(node[1], "loopi") self:emit_stmts(node[5] or node[4]) self:leave_scope() self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_Forin(node) self:emit_noop(node) self:emit_exprs(node[2]) self:enter_scope() self:register_label("do") self:emit_goto("break", true) self:enter_scope() self:emit(new_local_item(node[1])) self:register_vars(node[1], "loop") self:emit_stmts(node[3]) self:leave_scope() self:emit_noop(node, true) self:emit_goto("do") self:register_label("break") self:leave_scope() end function LinState:emit_stmt_If(node) self:emit_noop(node) self:enter_scope() for i = 1, #node - 1, 2 do self:enter_scope() self:emit_expr(node[i]) self:emit_cond_goto("else", node[i]) self:check_empty_block(node[i + 1]) self:emit_block(node[i + 1]) self:emit_goto("end") self:register_label("else") self:leave_scope() end if #node % 2 == 1 then self:check_empty_block(node[#node]) self:emit_block(node[#node]) end self:register_label("end") self:leave_scope() end function LinState:emit_stmt_Label(node) self:register_label(node[1], node.location, node.end_column) end function LinState:emit_stmt_Goto(node) self:emit_noop(node) self:emit_goto(node[1], false, node.location) end function LinState:emit_stmt_Break(node) self:emit_goto("break", false, node.location) end function LinState:emit_stmt_Return(node) self:emit_noop(node) self:emit_exprs(node) self:emit_goto("return") end function LinState:emit_expr(node) local item = new_eval_item(node) self:scan_expr(item, node) self:emit(item) end function LinState:emit_exprs(exprs) for _, expr in ipairs(exprs) do self:emit_expr(expr) end end LinState.emit_stmt_Call = LinState.emit_expr LinState.emit_stmt_Invoke = LinState.emit_expr function LinState:emit_stmt_Local(node) self:check_balance(node) local item = new_local_item(node[1], node[2], node.location, node.first_token) self:emit(item) if node[2] then self:scan_exprs(item, node[2]) end self:register_vars(node[1], "var") end function LinState:emit_stmt_Localrec(node) local item = new_local_item({node[1]}, {node[2]}, node.location, node.first_token) self:register_var(node[1], "var") self:emit(item) self:scan_expr(item, node[2]) end function LinState:emit_stmt_Set(node) self:check_balance(node) local item = new_set_item(node[1], node[2], node.location, node.first_token) self:scan_exprs(item, node[2]) for _, expr in ipairs(node[1]) do if expr.tag == "Id" then local var = self:check_var(expr) 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 self.chstate:warn_unused_field_value(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 returns it. -- Emits warnings: global, redefined/shadowed, unused field, unused label, unbalanced assignment, empty block. local function linearize(chstate, ast) local linstate = LinState(chstate) local line = linstate:build_line({{{tag = "Dots", "..."}}, ast}) assert(linstate.lines.size == 0) assert(linstate.scopes.size == 0) return line end return linearize luacheck-0.21.1/src/luacheck/love_standard.lua000066400000000000000000000133531315524664100212640ustar00rootroot00000000000000local standards = require "luacheck.standards" local empty = {} local read_write = {read_only = false} local love = { fields = { getVersion = empty, conf = read_write, directorydropped = read_write, draw = read_write, errhand = read_write, filedropped = read_write, focus = read_write, gamepadaxis = read_write, gamepadpressed = read_write, gamepadreleased = read_write, joystickadded = read_write, joystickaxis = read_write, joystickhat = read_write, joystickpressed = read_write, joystickreleased = read_write, joystickremoved = read_write, keypressed = read_write, keyreleased = read_write, load = read_write, lowmemory = read_write, mousefocus = read_write, mousemoved = read_write, mousepressed = read_write, mousereleased = read_write, quit = read_write, resize = read_write, run = read_write, textedited = read_write, textinput = read_write, threaderror = read_write, touchmoved = read_write, touchpressed = read_write, touchreleased = read_write, update = read_write, visible = read_write, 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"), event = standards.def_fields("clear","poll","pump","push","quit","wait"), filesystem = standards.def_fields("append","areSymlinksEnabled","createDirectory","exists", "getAppdataDirectory","getDirectoryItems","getIdentity","getLastModified", "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"), graphics = standards.def_fields("arc","circle","clear","discard","draw","ellipse","getBackgroundColor", "getBlendMode","getCanvas","getCanvasFormats","getColor","getColorMask", "getCompressedImageFormats","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", "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"), image = standards.def_fields("isCompressed","newCompressedData","newImageData"), joystick = standards.def_fields("getJoystickCount","getJoysticks","loadGamepadMappings", "saveGamepadMappings","setGamepadMapping"), keyboard = standards.def_fields("getKeyFromScancode","getScancodeFromKey","hasKeyRepeat","hasTextInput", "isDown","isScancodeDown","setKeyRepeat","setTextInput"), math = standards.def_fields("compress","decompress","gammaToLinear","getRandomSeed","getRandomState", "isConvex","linearToGamma","newBezierCurve","newRandomGenerator","noise","random", "randomNormal","setRandomSeed","setRandomState","triangulate"), mouse = standards.def_fields("getCursor","getPosition","getRelativeMode","getSystemCursor","getX", "getY","hasCursor","isDown","isGrabbed","isVisible","newCursor","setCursor","setGrabbed", "setPosition","setRelativeMode","setVisible","setX","setY"), physics = standards.def_fields("getDistance","getMeter","newBody","newChainShape","newCircleShape", "newDistanceJoint","newEdgeShape","newFixture","newFrictionJoint","newGearJoint", "newMotorJoint","newMouseJoint","newPolygonShape","newPrismaticJoint","newPulleyJoint", "newRectangleShape","newRevoluteJoint","newRopeJoint","newWeldJoint","newWheelJoint", "newWorld","setMeter"), sound = standards.def_fields("newDecoder","newSoundData"), system = standards.def_fields("getClipboardText","getOS","getPowerInfo","getProcessorCount","openURL", "setClipboardText","vibrate"), thread = standards.def_fields("getChannel","newChannel","newThread"), timer = standards.def_fields("getAverageDelta","getDelta","getFPS","getTime","sleep","step"), touch = standards.def_fields("getPosition","getPressure","getTouches"), video = standards.def_fields("newVideoStream"), window = standards.def_fields("close","fromPixels","getDisplayName","getFullscreen", "getFullscreenModes","getIcon","getMode","getPixelScale","getPosition","getTitle", "hasFocus","hasMouseFocus","isDisplaySleepEnabled","isMaximized","isOpen","isVisible", "maximize","minimize","requestAttention","setDisplaySleepEnabled","setFullscreen", "setIcon","setMode","setPosition","setTitle","showMessageBox","toPixels") } } -- `love` standard contains only `love` global, so return it here directly using normal std format. return { read_globals = {love = love} } luacheck-0.21.1/src/luacheck/lua_fs.lua000066400000000000000000000036421315524664100177100ustar00rootroot00000000000000local 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 luacheck-0.21.1/src/luacheck/main.lua000066400000000000000000000511621315524664100173630ustar00rootroot00000000000000local luacheck = require "luacheck" local argparse = require "luacheck.argparse" local builtin_standards = require "luacheck.builtin_standards" local config = require "luacheck.config" local options = require "luacheck.options" local expand_rockspec = require "luacheck.expand_rockspec" local multithreading = require "luacheck.multithreading" local cache = require "luacheck.cache" local format = require "luacheck.format" local version = require "luacheck.version" local fs = require "luacheck.fs" local globbing = require "luacheck.globbing" local utils = require "luacheck.utils" local exit_codes = { ok = 0, warnings = 1, errors = 2, fatals = 3, critical = 4 } local function critical(msg) io.stderr:write("Critical error: "..msg.."\n") os.exit(exit_codes.critical) end local function main() local default_cache_path = ".luacheckcache" local function get_parser() local parser = argparse("luacheck", "luacheck " .. luacheck._VERSION .. ", a simple static analyzer for Lua.", [[ Links: Luacheck on GitHub: https://github.com/mpeterv/luacheck Luacheck documentation: http://luacheck.readthedocs.org]]) parser:argument "files" :description (fs.has_lfs and [[List of files, directories and rockspecs to check. Pass "-" to check stdin.]] or [[List of files and rockspecs to check. Pass "-" to check stdin.]]) :args "+" :argname "" parser:flag("-g --no-global", [[Filter out warnings related to global variables. Equivalent to --ignore 1.]]):target("global"):action("store_false") parser:flag("-u --no-unused", [[Filter out warnings related to unused variables and values. Equivalent to --ignore [23].]]):target("unused"):action("store_false") parser:flag("-r --no-redefined", [[Filter out warnings related to redefined variables. Equivalent to --ignore 4.]]):target("redefined"):action("store_false") parser:flag("-a --no-unused-args", [[Filter out warnings related to unused arguments and loop variables. Equivalent to --ignore 21[23].]]):target("unused_args"):action("store_false") parser:flag("-s --no-unused-secondaries", [[Filter out warnings related to unused variables set together with used ones.]]):target("unused_secondaries"):action("store_false") parser:flag("--no-self", "Filter out warnings related to implicit self argument.") :target("self"):action("store_false") 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:option("--std", ([[Set standard globals, default is _G. can be one of: lua51 - globals of Lua 5.1 without deprecated ones; lua51c - globals of Lua 5.1; lua52 - globals of Lua 5.2; lua52c - globals of Lua 5.2 with LUA_COMPAT_ALL; lua53 - globals of Lua 5.3; lua53c - globals of Lua 5.3 with LUA_COMPAT_5_2; luajit - globals of LuaJIT 2.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 - 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; love - globals added by LOVE (love2d); busted - globals added by Busted 2.0; rockspec - globals allowed in rockspecs; none - no standard globals. Sets can be combined using "+".]]):format(default_std_name)) parser:option("--globals", [[Add custom global variables (e.g. foo) or fields (e.g. foo.bar) on top of standard ones.]]) :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--read-globals", "Add read-only global variables or fields.") :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--new-globals", [[Set custom global variables or fields. Removes custom globals added previously.]]) :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--new-read-globals", [[Set read-only global variables or fields. Removes read-only globals added previously.]]) :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:option("--not-globals", "Remove custom and standard global variables or fields.") :args "*" :count "*" :argname "" :action "concat" :init(nil) parser:flag("-c --compat", "Equivalent to --std max.") parser:flag("-d --allow-defined", "Allow defining globals implicitly by setting them.") parser:flag("-t --allow-defined-top", [[Allow defining globals implicitly by setting them in the top level scope.]]) parser:flag("-m --module", [[Limit visibility of implicitly defined globals to their files.]]) parser:option("--max-line-length", "Set maximum allowed line length (default: 120).") :argname "" :convert(tonumber) parser:flag("--no-max-line-length", "Do not limit line length.") :action "store_false" :target "max_line_length" parser:option("--max-code-line-length", [[Set maximum allowed length for lines ending with code (default: 120).]]) :argname "" :convert(tonumber) parser:flag("--no-max-code-line-length", "Do not limit code line length.") :action "store_false" :target "max_code_line_length" parser:option("--max-string-line-length", [[Set maximum allowed length for lines within a string (default: 120).]]) :argname "" :convert(tonumber) parser:flag("--no-max-string-line-length", "Do not limit string line length.") :action "store_false" :target "max_string_line_length" parser:option("--max-comment-line-length", [[Set maximum allowed length for comment lines (default: 120).]]) :argname "" :convert(tonumber) parser:flag("--no-max-comment-line-length", "Do not limit comment line length.") :action "store_false" :target "max_comment_line_length" parser:option("--ignore -i", [[Filter out warnings matching these patterns. If a pattern contains slash, part before slash matches warning code and part after it matches name of related variable. Otherwise, if the pattern contains letters or underscore, it matches name of related variable. Otherwise, the pattern matches warning code.]]) :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:option("--enable -e", "Do not filter out warnings matching these patterns.") :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:option("--only -o", "Filter out warnings not matching these patterns.") :args "+" :count "*" :argname "" :action "concat" :init(nil) parser:flag("--no-inline", "Disable inline options."):target("inline"):action("store_false") parser:mutex( parser:option("--config", "Path to configuration file. (default: "..config.default_path..")"), parser:flag("--no-config", "Do not look up configuration file.") ) local default_global_path = config.get_default_global_path() parser:mutex( parser:option("--default-config", ([[ Path to configuration file to use if --[no-]config is not used and project-specific %s is not found. (default: %s)]]):format(config.default_path, default_global_path or "could not detect")) :default(default_global_path) :show_default(false), parser:flag("--no-default-config", "Do not use default configuration file.") ) parser:option("--filename", [[Use another filename in output and for selecting configuration overrides.]]) parser:option("--exclude-files", "Do not check files matching these globbing patterns.") :args "+" :count "*" :argname "" parser:option("--include-files", [[Do not check files not matching these globbing patterns.]]) :args "+" :count "*" :argname "" if fs.has_lfs then parser:mutex( parser:option("--cache", "Path to cache file.", default_cache_path) :defmode "arg", parser:flag("--no-cache", "Do not use cache.") ) end local lanes_notice = "" if not multithreading.has_lanes then lanes_notice = "\nWarning: LuaLanes not found." end parser:option( "-j --jobs", "Check files in parallel (default: " .. tostring(multithreading.default_jobs) .. ")." .. lanes_notice):convert(tonumber) parser:option("--formatter" , [[Use custom formatter. must be a module name or one of: TAP - Test Anything Protocol formatter; JUnit - JUnit XML formatter; plain - simple warning-per-line formatter; default - standard formatter.]]) parser:flag("-q --quiet", [[Suppress output for files without warnings. -qq: Suppress output of warnings. -qqq: Only print total number of warnings and errors.]]) :count "0-3" parser:flag("--codes", "Show warning codes.") parser:flag("--ranges", "Show ranges of columns related to warnings.") parser:flag("--no-color", "Do not color output.") parser:flag("-v --version", "Show version info and exit.") :action(function() print(version.string) os.exit(exit_codes.ok) end) return parser end local function match_any(globs, name) for _, glob in ipairs(globs) do if globbing.match(glob, name) then return true end end return false end local function is_included(args, name) return not match_any(args.exclude_files, name) and ( #args.include_files == 0 or match_any(args.include_files, name)) end -- Expands folders, rockspecs, - -- Returns new array of filenames and table mapping indexes of bad files to {fatal = error type, msg = message}. -- Removes "./" in the beginnings of file names. -- Filters filenames using args.exclude_files and args.include_files. local function expand_files(args) -- If --include-files is used, do not focus on .lua files in directories. local dir_pattern = #args.include_files > 0 and "" or "%.lua$" local res, bad_files = {}, {} local function add(file) if type(file) == "string" then file = file:gsub("^%.[/\\]([^/])", "%1") end local name = args.filename or file if type(name) == "string" then if not is_included(args, name) then return false end end table.insert(res, file) return true end for _, file in ipairs(args.files) do if file == "-" then add(io.stdin) elseif fs.is_dir(file) then if fs.has_lfs then local extracted, err = fs.extract_files(file, dir_pattern) if extracted then for _, nested_file in ipairs(extracted) do add(nested_file) end elseif add(file) then bad_files[#res] = {fatal = "I/O", msg = err} end elseif add(file) then bad_files[#res] = {fatal = "I/O", msg = "LuaFileSystem required to check directories"} end elseif file:sub(-#".rockspec") == ".rockspec" then local related_files, err, msg = expand_rockspec(file) if related_files then for _, related_file in ipairs(related_files) do add(related_file) end elseif add(file) then bad_files[#res] = {fatal = err, msg = msg} end else add(file) end end return res, bad_files end local function validate_args(args, parser) if args.jobs and args.jobs < 1 then parser:error(" must be at least 1") end if args.std and not options.split_std(args.std) then parser:error(" must name a standard library") end end local function combine_conf_and_args_path_arrays(conf, args, option) local conf_opts = config.get_top_options(conf) if conf_opts[option] then for i, path in ipairs(conf_opts[option]) do conf_opts[option][i] = config.relative_path(conf, path) end table.insert(args[option], conf_opts[option]) end args[option] = utils.concat_arrays(args[option]) end -- Applies cli-specific options from config to args. local function combine_config_and_args(conf, args) local conf_opts = config.get_top_options(conf) if args.no_color then args.color = false else args.color = conf_opts.color ~= false end args.codes = args.codes or conf_opts.codes args.formatter = args.formatter or conf_opts.formatter or "default" if args.no_cache or not fs.has_lfs then args.cache = false elseif not args.cache then if type(conf_opts.cache) == "string" then args.cache = config.relative_path(conf, conf_opts.cache) else args.cache = conf_opts.cache end end if args.cache == true then args.cache = config.relative_path(conf, default_cache_path) end args.jobs = args.jobs or conf_opts.jobs or multithreading.default_jobs combine_conf_and_args_path_arrays(conf, args, "exclude_files") combine_conf_and_args_path_arrays(conf, args, "include_files") end -- Returns sparse array of mtimes and map of filenames to cached reports. local function get_mtimes_and_cached_reports(cache_filename, files, bad_files) local cache_files = {} local cache_mtimes = {} local sparse_mtimes = {} for i, file in ipairs(files) do if not bad_files[i] and file ~= io.stdin then table.insert(cache_files, file) local mtime = fs.get_mtime(file) table.insert(cache_mtimes, mtime) sparse_mtimes[i] = mtime end end return sparse_mtimes, cache.load(cache_filename, cache_files, cache_mtimes) or critical( ("Couldn't load cache from %s: data corrupted"):format(cache_filename)) end -- Returns sparse array of sources of files that need to be checked, -- updates bad_files with files that had I/O issues. local function get_srcs_to_check(cached_reports, files, bad_files) local res = {} for i, file in ipairs(files) do if not bad_files[i] and not cached_reports[file] then local src, err_msg = utils.read_file(file) if src then res[i] = src else bad_files[i] = {fatal = "I/O", msg = err_msg} end end end return res end -- Returns sparse array of new reports. local function get_new_reports(files, srcs, jobs) local dense_srcs = {} local dense_to_sparse = {} for i in ipairs(files) do if srcs[i] then table.insert(dense_srcs, srcs[i]) dense_to_sparse[#dense_srcs] = i end end local map = jobs and multithreading.has_lanes and multithreading.pmap or utils.map local dense_res = map(luacheck.get_report, dense_srcs, jobs) local res = {} for i in ipairs(dense_srcs) do res[dense_to_sparse[i]] = dense_res[i] end return res end -- Updates cache with new_reports. Updates bad_files for which mtime is absent. local function update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports) local cache_files = {} local cache_mtimes = {} local cache_reports = {} for i, file in ipairs(files) do if srcs[i] and file ~= io.stdin then if not mtimes[i] then bad_files[i] = {fatal = "I/O", msg = "couldn't get modification time"} else table.insert(cache_files, file) table.insert(cache_mtimes, mtimes[i]) table.insert(cache_reports, new_reports[i] or false) end end end return cache.update(cache_filename, cache_files, cache_mtimes, cache_reports) or critical( ("Couldn't save cache to %s: I/O error"):format(cache_filename)) end -- Returns array of reports for files. local function get_reports(cache_filename, files, bad_files, jobs) local mtimes local cached_reports if cache_filename then mtimes, cached_reports = get_mtimes_and_cached_reports(cache_filename, files, bad_files) else cached_reports = {} end local srcs = get_srcs_to_check(cached_reports, files, bad_files) local new_reports = get_new_reports(files, srcs, jobs) if cache_filename then update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports) end local res = {} for i, file in ipairs(files) do if bad_files[i] then res[i] = bad_files[i] else res[i] = cached_reports[file] or new_reports[i] end end return res end local function combine_config_and_options(conf, cli_opts, files) local res = {} for i, file in ipairs(files) do res[i] = config.get_options(conf, file) table.insert(res[i], cli_opts) end return res end local function substitute_filename(new_filename, files) if new_filename then for i = 1, #files do files[i] = new_filename end end end local function normalize_stdin_in_filenames(files) for i, file in ipairs(files) do if type(file) ~= "string" then files[i] = "stdin" end end end local builtin_formatters = utils.array_to_set({"TAP", "JUnit", "plain", "default"}) local function pformat(report, file_names, conf, args) if builtin_formatters[args.formatter] then return format.format(report, file_names, args) end local formatter = args.formatter local ok, output if type(formatter) == "string" then ok, formatter = config.relative_require(conf, formatter) if not ok then critical(("Couldn't load custom formatter '%s': %s"):format(args.formatter, formatter)) end end ok, output = pcall(formatter, report, file_names, args) if not ok then critical(("Couldn't run custom formatter '%s': %s"):format(tostring(args.formatter), output)) end return output end local parser = get_parser() local ok, args = parser:pparse() if not ok then io.stderr:write(("%s\n\nError: %s\n"):format(parser:get_usage(), args)) os.exit(exit_codes.critical) end local conf if args.no_config then conf = config.empty_config else local err conf, err = config.load_config(args.config, not args.no_default_config and args.default_config) if not conf then critical(err) end end -- Validate args only after loading config so that custom stds are already in place. validate_args(args, parser) combine_config_and_args(conf, args) local files, bad_files = expand_files(args) local reports = get_reports(args.cache, files, bad_files, args.jobs) substitute_filename(args.filename, files) local report = luacheck.process_reports(reports, combine_config_and_options(conf, args, files)) normalize_stdin_in_filenames(files) local output = pformat(report, files, conf, args) if #output > 0 and output:sub(-1) ~= "\n" then output = output .. "\n" end io.stdout:write(output) if report.fatals > 0 then os.exit(exit_codes.fatals) elseif report.errors > 0 then os.exit(exit_codes.errors) elseif report.warnings > 0 then os.exit(exit_codes.warnings) else os.exit(exit_codes.ok) end end local _, error_wrapper = utils.try(main) local err = error_wrapper.err local traceback = error_wrapper.traceback if utils.is_instance(err, utils.InvalidPatternError) then critical(("Invalid pattern '%s'"):format(err.pattern)) elseif type(err) == "string" and err:match("interrupted!$") then critical("Interrupted") else local msg = ("Luacheck %s bug (please report at github.com/mpeterv/luacheck/issues):\n%s\n%s"):format( luacheck._VERSION, err, traceback) critical(msg) end luacheck-0.21.1/src/luacheck/multithreading.lua000066400000000000000000000045361315524664100214620ustar00rootroot00000000000000local utils = require "luacheck.utils" local multithreading = {} local lanes_ok, lanes = pcall(require, "lanes") lanes_ok = lanes_ok and pcall(lanes.configure) multithreading.has_lanes = lanes_ok multithreading.lanes = lanes multithreading.default_jobs = 1 if not lanes_ok then return multithreading end local cpu_number_detection_commands = {} if utils.is_windows then cpu_number_detection_commands[1] = "echo %NUMBER_OF_PROCESSORS%" else cpu_number_detection_commands[1] = "getconf _NPROCESSORS_ONLN 2>&1" cpu_number_detection_commands[2] = "sysctl -n hw.ncpu 2>&1" cpu_number_detection_commands[3] = "psrinfo -p 2>&1" end for _, command in ipairs(cpu_number_detection_commands) do local handler = io.popen(command) if handler then local output = handler:read("*a") handler:close() if output then local cpu_number = tonumber(utils.strip(output)) if cpu_number then multithreading.default_jobs = math.floor(math.max(cpu_number, 1)) break end end end end -- Reads pairs {key, arg} from given linda slot until it gets nil as arg. -- Returns table with pairs [key] = func(arg). local function worker_task(linda, input_slot, func) local results = {} while true do local _, pair = linda:receive(nil, input_slot) local key, arg = pair[1], pair[2] if arg == nil then return results end results[key] = func(arg) end end local function protected_worker_task(...) return true, utils.try(worker_task, ...) end local worker_gen = lanes.gen("*", protected_worker_task) -- Maps func over array, performing at most jobs calls in parallel. function multithreading.pmap(func, array, jobs) jobs = math.min(jobs, #array) if jobs < 2 then return utils.map(func, array) end local workers = {} local linda = lanes.linda() for i = 1, jobs do workers[i] = worker_gen(linda, 0, func) end for i, item in ipairs(array) do linda:send(nil, 0, {i, item}) end for _ = 1, jobs do linda:send(nil, 0, {}) end local results = {} for _, worker in ipairs(workers) do local _, ok, worker_results = assert(worker:join()) if ok then utils.update(results, worker_results) else error(worker_results, 0) end end return results end return multithreading luacheck-0.21.1/src/luacheck/ngx_standard.lua000066400000000000000000000126641315524664100211170ustar00rootroot00000000000000local standards = require "luacheck.standards" local empty = {} local luajit_string_def = standards.def_fields("byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", "reverse", "sub", "upper") -- Globals added by lua-nginx-module 0.10.10 in internal definition table format. -- Will be added to `luajit` std to form `ngx_lua` std. local ngx_defs = { fields = { ngx = { fields = { arg = {other_fields = true, read_only = false}, var = {other_fields = true, read_only = false}, OK = empty, ERROR = empty, AGAIN = empty, DONE = empty, DECLINED = empty, null = empty, HTTP_GET = empty, HTTP_HEAD = empty, HTTP_PUT = empty, HTTP_POST = empty, HTTP_DELETE = empty, HTTP_OPTIONS = empty, HTTP_MKCOL = empty, HTTP_COPY = empty, HTTP_MOVE = empty, HTTP_PROPFIND = empty, HTTP_PROPPATCH = empty, HTTP_LOCK = empty, HTTP_UNLOCK = empty, HTTP_PATCH = empty, HTTP_TRACE = empty, HTTP_CONTINUE = empty, HTTP_SWITCHING_PROTOCOLS = empty, HTTP_OK = empty, HTTP_CREATED = empty, HTTP_ACCEPTED = empty, HTTP_NO_CONTENT = empty, HTTP_PARTIAL_CONTENT = empty, HTTP_SPECIAL_RESPONSE = empty, HTTP_MOVED_PERMANENTLY = empty, HTTP_MOVED_TEMPORARILY = empty, HTTP_SEE_OTHER = empty, HTTP_NOT_MODIFIED = empty, HTTP_TEMPORARY_REDIRECT = empty, HTTP_BAD_REQUEST = empty, HTTP_UNAUTHORIZED = empty, HTTP_PAYMENT_REQUIRED = empty, HTTP_FORBIDDEN = empty, HTTP_NOT_FOUND = empty, HTTP_NOT_ALLOWED = empty, HTTP_NOT_ACCEPTABLE = empty, HTTP_REQUEST_TIMEOUT = empty, HTTP_CONFLICT = empty, HTTP_GONE = empty, HTTP_UPGRADE_REQUIRED = empty, HTTP_TOO_MANY_REQUESTS = empty, HTTP_CLOSE = empty, HTTP_ILLEGAL = empty, HTTP_INTERNAL_SERVER_ERROR = empty, HTTP_METHOD_NOT_IMPLEMENTED = empty, HTTP_BAD_GATEWAY = empty, HTTP_SERVICE_UNAVAILABLE = empty, HTTP_GATEWAY_TIMEOUT = empty, HTTP_VERSION_NOT_SUPPORTED = empty, HTTP_INSUFFICIENT_STORAGE = empty, STDERR = empty, EMERG = empty, ALERT = empty, CRIT = empty, ERR = empty, WARN = empty, NOTICE = empty, INFO = empty, DEBUG = empty, ctx = {other_fields = true, read_only = false}, location = standards.def_fields("capture", "capture_multi"), status = {read_only = false}, header = {other_fields = true, read_only = false}, resp = standards.def_fields("get_headers"), req = standards.def_fields("is_internal", "start_time", "http_version", "raw_header", "get_method", "set_method", "set_uri", "set_uri_args", "get_uri_args", "get_post_args", "get_headers", "set_header", "clear_header", "read_body", "discard_body", "get_body_data", "get_body_file", "set_body_data", "set_body_file", "init_body", "append_body", "finish_body", "socket"), exec = empty, redirect = empty, send_headers = empty, headers_sent = empty, print = empty, say = empty, log = empty, flush = empty, exit = empty, eof = empty, sleep = empty, escape_uri = empty, unescape_uri = empty, encode_args = empty, decode_args = empty, encode_base64 = empty, decode_base64 = empty, crc32_short = empty, crc32_long = empty, hmac_sha1 = empty, md5 = empty, md5_bin = empty, sha1_bin = empty, quote_sql_str = empty, today = empty, time = empty, now = empty, update_time = empty, localtime = empty, utctime = empty, cookie_time = empty, http_time = empty, parse_http_time = empty, is_subrequest = empty, re = standards.def_fields("match", "find", "gmatch", "sub", "gsub"), shared = {other_fields = true, read_only = false}, socket = standards.def_fields("udp", "tcp", "connect", "stream"), get_phase = empty, thread = standards.def_fields("spawn", "wait", "kill"), on_abort = empty, timer = standards.def_fields("at", "every", "running_count", "pending_count"), config = { fields = { subsystem = luajit_string_def, debug = empty, prefix = empty, nginx_version = empty, nginx_configure = empty, ngx_lua_version = empty, } }, worker = standards.def_fields("pid", "count", "id", "exiting"), }, }, ndk = { fields = { set_var = {other_fields = true}, }, }, }, } return ngx_defs luacheck-0.21.1/src/luacheck/options.lua000066400000000000000000000265561315524664100201430ustar00rootroot00000000000000local options = {} local builtin_standards = require "luacheck.builtin_standards" local standards = require "luacheck.standards" local utils = require "luacheck.utils" local boolean = utils.has_type("boolean") local array_of_strings = utils.array_of("string") function options.split_std(std) local parts = utils.split(std, "+") if parts[1]:match("^%s*$") then parts.add = true table.remove(parts, 1) end for i, part in ipairs(parts) do parts[i] = utils.strip(part) if not builtin_standards[parts[i]] then return end end return parts end local function std_or_array_of_strings(x) return (type(x) == "string" and options.split_std(x)) or standards.validate_std_table(x) end local function field_map(x) return standards.validate_std_table({globals = x}) end local function number_or_false(x) return x == false or type(x) == "number" end function options.add_order(option_set) local opts = {} for option in pairs(option_set) do if type(option) == "string" then table.insert(opts, option) end end table.sort(opts) utils.update(option_set, opts) end options.nullary_inline_options = { global = boolean, unused = boolean, redefined = boolean, unused_args = boolean, unused_secondaries = boolean, self = boolean, compat = boolean, allow_defined = boolean, allow_defined_top = boolean, module = boolean } options.variadic_inline_options = { globals = field_map, read_globals = field_map, new_globals = field_map, new_read_globals = field_map, not_globals = array_of_strings, ignore = array_of_strings, enable = array_of_strings, only = array_of_strings } options.all_options = { std = std_or_array_of_strings, max_line_length = number_or_false, max_code_line_length = number_or_false, max_string_line_length = number_or_false, max_comment_line_length = number_or_false, inline = boolean } utils.update(options.all_options, options.nullary_inline_options) utils.update(options.all_options, options.variadic_inline_options) options.add_order(options.all_options) -- Returns true if opts is valid option_set. -- Otherwise returns false and, optionally, name of the problematic option. function options.validate(option_set, opts) if opts == nil then return true end local ok, is_valid, invalid_opt = pcall(function() assert(type(opts) == "table") for _, option in ipairs(option_set) do if opts[option] ~= nil then if not option_set[option](opts[option]) then return false, option end end end return true end) return ok and is_valid, invalid_opt end -- Option stack is an array of options with options closer to end -- overriding options closer to beginning. -- Extracts sequence of active std tables from an option stack. local function get_std_tables(opts_stack) local base_std local add_stds = {} local no_compat = false for _, opts in utils.ripairs(opts_stack) do if opts.compat and not no_compat then base_std = builtin_standards.max break elseif opts.compat == false then no_compat = true end if opts.std then if type(opts.std) == "table" then base_std = opts.std break else local parts = options.split_std(opts.std) for _, part in ipairs(parts) do table.insert(add_stds, builtin_standards[part]) end if not parts.add then base_std = {} break end end end end table.insert(add_stds, 1, base_std or builtin_standards._G) return add_stds end -- Returns index of the last option table in a stack that uses given option, -- or zero if the option isn't used anywhere. local function index_of_last_option_usage(opts_stack, option_name) for index, opts in utils.ripairs(opts_stack) do if opts[option_name] then return index end end return 0 end local function split_field(field_name) return utils.split(field_name, "%.") end local function field_comparator(field1, field2) local parts1 = field1[1] local parts2 = field2[1] for i = 1, math.max(#parts1, #parts2) do local part1 = parts1[i] local part2 = parts2[i] if not part1 then return true elseif not part2 then return false end if part1 ~= part2 then return part1 < part2 end end return false end -- Combine all stds and global related options into one final definition table. -- A definition table may have fields `read_only` (boolean), `other_fields` (boolean), -- and `fields` (maps field names to definition tables). -- Std table format is similar, except at the top level there are two fields -- `globals` and `read_globals` mapping to top-level field tables. Also in field tables -- it's possible to use field names in array part as a shortcut: -- `{fields = {"foo"}}` is equivalent to `{fields = {foo = {}}}` or `{fields = {foo = {other_fields = true}}}` -- in top level fields tables. local function get_final_std(opts_stack) local final_std = {} local std_tables = get_std_tables(opts_stack) for _, std_table in ipairs(std_tables) do standards.add_std_table(final_std, std_table) end local last_new_globals = index_of_last_option_usage(opts_stack, "new_globals") local last_new_read_globals = index_of_last_option_usage(opts_stack, "new_read_globals") for index, opts in ipairs(opts_stack) do local globals = (index >= last_new_globals) and (opts.new_globals or opts.globals) local read_globals = (index >= last_new_read_globals) and (opts.new_read_globals or opts.read_globals) local new_fields = {} if globals then for _, global in ipairs(globals) do table.insert(new_fields, {split_field(global), false}) end end if read_globals then for _, read_global in ipairs(read_globals) do table.insert(new_fields, {split_field(read_global), true}) end end if globals and read_globals then -- If there are both globals and read-only globals defined in one options table, -- it's important that more general definitions are applied first, -- otherwise they will completely overwrite more specific definitions. -- E.g. `globals x` should be applied before `read globals x.y`. table.sort(new_fields, field_comparator) end for _, field in ipairs(new_fields) do standards.overwrite_field(final_std, field[1], field[2]) end standards.add_std_table(final_std, {globals = globals, read_globals = read_globals}, true, true) if opts.not_globals then for _, not_global in ipairs(opts.not_globals) do standards.remove_field(final_std, split_field(not_global)) end end end standards.finalize(final_std) return final_std end local function get_scalar_opt(opts_stack, option, default) for _, opts in utils.ripairs(opts_stack) do if opts[option] ~= nil then return opts[option] end end return default end local line_length_suboptions = {"max_code_line_length", "max_string_line_length", "max_comment_line_length"} local function get_max_line_opts(opts_stack) local res = {max_line_length = 120} for _, opt_name in ipairs(line_length_suboptions) do res[opt_name] = res.max_line_length end for _, opts in ipairs(opts_stack) do if opts.max_line_length ~= nil then res.max_line_length = opts.max_line_length for _, opt_name in ipairs(line_length_suboptions) do res[opt_name] = opts.max_line_length end end for _, opt_name in ipairs(line_length_suboptions) do if opts[opt_name] ~= nil then res[opt_name] = opts[opt_name] end end end return res end local function anchor_pattern(pattern, only_start) if not pattern then return end if pattern:sub(1, 1) == "^" or pattern:sub(-1) == "$" then return pattern else return "^" .. pattern .. (only_start and "" or "$") end end -- Returns {pair of normalized patterns for code and name}. -- `pattern` can be: -- string containing '/': first part matches warning code, second - variable name; -- string containing letters: matches variable name; -- otherwise: matches warning code. -- Unless anchored by user, pattern for name is anchored from both sides -- and pattern for code is only anchored at the beginning. local function normalize_pattern(pattern) local code_pattern, name_pattern local slash_pos = pattern:find("/") if slash_pos then code_pattern = pattern:sub(1, slash_pos - 1) name_pattern = pattern:sub(slash_pos + 1) elseif pattern:find("[_a-zA-Z]") then name_pattern = pattern else code_pattern = pattern end return {anchor_pattern(code_pattern, true), anchor_pattern(name_pattern)} end -- From most specific to less specific, pairs {option, pattern}. -- Applying macros in order is required to get deterministic results -- and get sensible results when intersecting macros are used. -- E.g. unused = false, unused_args = true should leave unused args enabled. local macros = { {"unused_args", "21[23]"}, {"global", "1"}, {"unused", "[23]"}, {"redefined", "4"} } -- Returns array of rules which should be applied in order. -- A rule is a table {{pattern*}, type}. -- `pattern` is a non-normalized pattern. -- `type` can be "enable", "disable" or "only". local function get_rules(opts_stack) local rules = {} local used_macros = {} for _, opts in utils.ripairs(opts_stack) do for _, macro_info in ipairs(macros) do local option, pattern = macro_info[1], macro_info[2] if not used_macros[option] then if opts[option] ~= nil then table.insert(rules, {{pattern}, opts[option] and "enable" or "disable"}) used_macros[option] = true end end end if opts.ignore then table.insert(rules, {opts.ignore, "disable"}) end if opts.only then table.insert(rules, {opts.only, "only"}) end if opts.enable then table.insert(rules, {opts.enable, "enable"}) end end return rules end local function normalize_patterns(rules) local res = {} for i, rule in ipairs(rules) do res[i] = {{}, rule[2]} for j, pattern in ipairs(rule[1]) do res[i][1][j] = normalize_pattern(pattern) end end return res end local scalar_options = { unused_secondaries = true, self = true, inline = true, module = false, allow_defined = false, allow_defined_top = false } -- 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; -- max_line_length: number or false; -- rules: see get_rules. function options.normalize(opts_stack) local res = {} res.std = get_final_std(opts_stack) for option, default in pairs(scalar_options) do res[option] = get_scalar_opt(opts_stack, option, default) end local max_line_opts = get_max_line_opts(opts_stack) utils.update(res, max_line_opts) res.rules = normalize_patterns(get_rules(opts_stack)) return res end return options luacheck-0.21.1/src/luacheck/parser.lua000066400000000000000000000550051315524664100177330ustar00rootroot00000000000000local lexer = require "luacheck.lexer" local utils = require "luacheck.utils" 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 parser.SyntaxError = utils.class() function parser.SyntaxError:__init(loc, end_column, msg) self.line = loc.line self.column = loc.column self.end_column = end_column self.msg = msg end function parser.syntax_error(loc, end_column, msg) error(parser.SyntaxError(loc, end_column, msg), 0) end local function token_body_or_line(state) return state.lexer.src:sub(state.offset, state.lexer.offset - 1):match("^[^\r\n]*") end local function mark_line_endings(state, first_line, last_line, token_type) for line = first_line, last_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 } mark_line_endings(state, state.line, state.lexer.line, "comment") else if state.token ~= "eof" then mark_line_endings(state, state.line, state.lexer.line, "string") state.code_lines[state.line] = true state.code_lines[state.lexer.line] = true end break end end end local function init_ast_node(node, loc, tag) node.location = loc node.tag = tag return node end local function new_ast_node(state, tag) return init_ast_node({}, location(state), tag) end local token_names = { eof = "", name = "identifier", ["do"] = "'do'", ["end"] = "'end'", ["then"] = "'then'", ["in"] = "'in'", ["until"] = "'until'", ["::"] = "'::'" } local function token_name(token) return token_names[token] or lexer.quote(token) end local function parse_error(state, msg) local token_repr, end_column if state.token == "eof" then token_repr = "" end_column = state.column else token_repr = token_body_or_line(state) end_column = state.column + #token_repr - 1 token_repr = lexer.quote(token_repr) end parser.syntax_error(state, end_column, msg .. " near " .. token_repr) end local function check_token(state, token) if state.token ~= token then parse_error(state, "expected " .. token_name(token)) end end local function check_and_skip_token(state, token) check_token(state, token) skip_token(state) end local function test_and_skip_token(state, token) if state.token == token then skip_token(state) return true end end local function check_closing_token(state, opening_token, closing_token, opening_line) if state.token ~= closing_token then local err = "expected " .. token_name(closing_token) if opening_line ~= state.line then err = err .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_line) .. ")" end parse_error(state, err) end skip_token(state) end local function check_name(state) check_token(state, "name") return state.token_value end -- If needed, wraps last expression in expressions in "Paren" node. local function opt_add_parens(expressions, is_inside_parentheses) if is_inside_parentheses then local last = expressions[#expressions] if last and last.tag == "Call" or last.tag == "Invoke" or last.tag == "Dots" then expressions[#expressions] = init_ast_node({last}, last.location, "Paren") end end end local parse_block, parse_expression local function parse_expression_list(state) local list = {} local is_inside_parentheses repeat list[#list+1], is_inside_parentheses = parse_expression(state) until not test_and_skip_token(state, ",") opt_add_parens(list, is_inside_parentheses) return list end local function parse_id(state, tag) local ast_node = new_ast_node(state, tag or "Id") ast_node[1] = check_name(state) skip_token(state) -- Skip name. return ast_node end local function atom(tag) return function(state) local ast_node = new_ast_node(state, tag) ast_node[1] = state.token_value skip_token(state) return ast_node end end local simple_expressions = {} simple_expressions.number = atom("Number") simple_expressions.string = atom("String") simple_expressions["nil"] = atom("Nil") simple_expressions["true"] = atom("True") simple_expressions["false"] = atom("False") simple_expressions["..."] = atom("Dots") simple_expressions["{"] = function(state) local ast_node = new_ast_node(state, "Table") local start_line = state.line skip_token(state) local is_inside_parentheses = false repeat if state.token == "}" then break else local lhs, rhs local item_location = location(state) local first_key_token if state.token == "name" then local name = state.token_value skip_token(state) -- Skip name. 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.line) check_and_skip_token(state, "=") rhs = parse_expression(state) else -- Expression in array part. rhs, is_inside_parentheses = parse_expression(state, nil, true) 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 end until not (test_and_skip_token(state, ",") or test_and_skip_token(state, ";")) check_closing_token(state, "{", "}", start_line) opt_add_parens(ast_node, is_inside_parentheses) return ast_node end -- Parses argument list and the statements. local function parse_function(state, func_location) local paren_line = state.line check_and_skip_token(state, "(") local args = {} if state.token ~= ")" then -- Are there arguments? repeat if state.token == "name" then args[#args+1] = parse_id(state) elseif state.token == "..." then args[#args+1] = simple_expressions["..."](state) break else parse_error(state, "expected argument") end until not test_and_skip_token(state, ",") end check_closing_token(state, "(", ")", paren_line) local body = parse_block(state) local end_location = location(state) check_closing_token(state, "function", "end", func_location.line) return init_ast_node({args, body, end_location = end_location}, func_location, "Function") end simple_expressions["function"] = function(state) local function_location = location(state) skip_token(state) -- Skip "function". return parse_function(state, function_location) end local calls = {} calls["("] = function(state) local paren_line = state.line skip_token(state) -- Skip "(". local args = (state.token == ")") and {} or parse_expression_list(state) check_closing_token(state, "(", ")", paren_line) return args end calls["{"] = function(state) return {simple_expressions[state.token](state)} end calls.string = calls["{"] local suffixes = {} suffixes["."] = function(state, lhs) skip_token(state) -- Skip ".". local rhs = parse_id(state, "String") return init_ast_node({lhs, rhs}, lhs.location, "Index") end suffixes["["] = function(state, lhs) local bracket_line = state.line skip_token(state) -- Skip "[". local rhs = parse_expression(state) check_closing_token(state, "[", "]", bracket_line) return init_ast_node({lhs, rhs}, lhs.location, "Index") end suffixes[":"] = function(state, lhs) skip_token(state) -- Skip ":". local method_name = parse_id(state, "String") local args = (calls[state.token] or parse_error)(state, "expected method arguments") table.insert(args, 1, lhs) table.insert(args, 2, method_name) return init_ast_node(args, lhs.location, "Invoke") end suffixes["("] = function(state, lhs) local args = calls[state.token](state) table.insert(args, 1, lhs) return init_ast_node(args, lhs.location, "Call") end suffixes["{"] = suffixes["("] suffixes.string = suffixes["("] -- Additionally returns whether 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 if state.token == "(" then in_parens = true local paren_line = state.line skip_token(state) local _ expression, _, first_token = parse_expression(state) check_closing_token(state, "(", ")", paren_line) elseif state.token == "name" then expression = parse_id(state) first_token = expression[1] else local literal_handler = simple_expressions[state.token] if not literal_handler or no_literals then parse_error(state, "expected " .. (kind or "expression")) end first_token = token_body_or_line(state) return literal_handler(state), false, first_token end while true do local suffix_handler = suffixes[state.token] if suffix_handler then in_parens = false expression = suffix_handler(state, expression) else return expression, in_parens, first_token end end end local unary_operators = { ["not"] = "not", ["-"] = "unm", -- Not mentioned in Metalua documentation. ["~"] = "bnot", ["#"] = "len" } local unary_priority = 12 local binary_operators = { ["+"] = "add", ["-"] = "sub", ["*"] = "mul", ["%"] = "mod", ["^"] = "pow", ["/"] = "div", ["//"] = "idiv", ["&"] = "band", ["|"] = "bor", ["~"] = "bxor", ["<<"] = "shl", [">>"] = "shr", [".."] = "concat", ["~="] = "ne", ["=="] = "eq", ["<"] = "lt", ["<="] = "le", [">"] = "gt", [">="] = "ge", ["and"] = "and", ["or"] = "or" } local left_priorities = { add = 10, sub = 10, mul = 11, mod = 11, pow = 14, div = 11, idiv = 11, band = 6, bor = 4, bxor = 5, shl = 7, shr = 7, concat = 9, ne = 3, eq = 3, lt = 3, le = 3, gt = 3, ge = 3, ["and"] = 2, ["or"] = 1 } local right_priorities = { add = 10, sub = 10, mul = 11, mod = 11, pow = 13, div = 11, idiv = 11, band = 6, bor = 4, bxor = 5, shl = 7, shr = 7, concat = 8, ne = 3, eq = 3, lt = 3, le = 3, gt = 3, ge = 3, ["and"] = 2, ["or"] = 1 } -- Additionally returns whether subexpression is 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") else expression, in_parens, first_token = parse_simple_expression(state, kind) end -- Expand while operators have priorities higher than `limit`. while true do local binary_operator = binary_operators[state.token] if not binary_operator or left_priorities[binary_operator] <= limit then break end in_parens = false skip_token(state) -- Skip operator. -- Read subexpression with higher priority. local subexpression = parse_subexpression(state, right_priorities[binary_operator]) expression = init_ast_node({binary_operator, expression, subexpression}, expression.location, "Op") end return expression, in_parens, first_token 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 end local statements = {} statements["if"] = function(state, loc) local start_line, start_token local next_line, next_token = loc.line, "if" local ast_node = init_ast_node({}, loc, "If") repeat ast_node[#ast_node+1] = parse_expression(state, "condition", true) local branch_location = location(state) check_and_skip_token(state, "then") ast_node[#ast_node+1] = parse_block(state, branch_location) start_line, start_token = next_line, next_token next_line, next_token = state.line, state.token until not test_and_skip_token(state, "elseif") if state.token == "else" then start_line, start_token = next_line, next_token local branch_location = location(state) skip_token(state) ast_node[#ast_node+1] = parse_block(state, branch_location) end check_closing_token(state, start_token, "end", start_line) return ast_node end statements["while"] = function(state, loc) local condition = parse_expression(state, "condition") check_and_skip_token(state, "do") local block = parse_block(state) check_closing_token(state, "while", "end", loc.line) return init_ast_node({condition, block}, loc, "While") end statements["do"] = function(state, loc) local ast_node = init_ast_node(parse_block(state), loc, "Do") check_closing_token(state, "do", "end", loc.line) return ast_node end statements["for"] = function(state, loc) local ast_node = init_ast_node({}, loc) -- Will set ast_node.tag later. local first_var = parse_id(state) if state.token == "=" then -- Numeric "for" loop. ast_node.tag = "Fornum" skip_token(state) ast_node[1] = first_var ast_node[2] = parse_expression(state) check_and_skip_token(state, ",") ast_node[3] = parse_expression(state) if test_and_skip_token(state, ",") then ast_node[4] = parse_expression(state) end check_and_skip_token(state, "do") ast_node[#ast_node+1] = parse_block(state) elseif state.token == "," or state.token == "in" then -- Generic "for" loop. ast_node.tag = "Forin" local iter_vars = {first_var} while test_and_skip_token(state, ",") do iter_vars[#iter_vars+1] = parse_id(state) end ast_node[1] = iter_vars check_and_skip_token(state, "in") ast_node[2] = parse_expression_list(state) check_and_skip_token(state, "do") ast_node[3] = parse_block(state) else parse_error(state, "expected '=', ',' or 'in'") end check_closing_token(state, "for", "end", loc.line) return ast_node end statements["repeat"] = function(state, loc) local block = parse_block(state) check_closing_token(state, "repeat", "until", loc.line) local condition = parse_expression(state, "condition", true) return init_ast_node({block, condition}, loc, "Repeat") end statements["function"] = function(state, loc) local lhs_location = location(state) local lhs = parse_id(state) local self_location while (not self_location) and (state.token == "." or state.token == ":") do self_location = state.token == ":" and location(state) skip_token(state) -- Skip "." or ":". lhs = init_ast_node({lhs, parse_id(state, "String")}, lhs_location, "Index") end local function_node = parse_function(state, loc) if self_location then -- Insert implicit "self" argument. local self_arg = init_ast_node({"self", implicit = true}, self_location, "Id") table.insert(function_node[1], 1, self_arg) end return init_ast_node({{lhs}, {function_node}}, loc, "Set") end statements["local"] = function(state, loc) if state.token == "function" then -- Localrec local function_location = location(state) skip_token(state) -- Skip "function". local var = parse_id(state) local function_node = parse_function(state, function_location) -- Metalua would return {{var}, {function}} for some reason. return init_ast_node({var, function_node}, loc, "Localrec") end local lhs = {} local rhs repeat lhs[#lhs+1] = parse_id(state) until not test_and_skip_token(state, ",") local equals_location = location(state) if test_and_skip_token(state, "=") then rhs = parse_expression_list(state) end -- According to Metalua spec, {lhs} should be returned if there is no rhs. -- Metalua does not follow the spec itself and returns {lhs, {}}. return init_ast_node({lhs, rhs, equals_location = rhs and equals_location}, loc, "Local") end statements["::"] = function(state, loc) local end_column = loc.column + 1 local name = check_name(state) if state.line == loc.line then -- Label name on the same line as opening `::`, pull token end to name end. end_column = state.column + #state.token_value - 1 end skip_token(state) -- Skip label name. if state.line == loc.line then -- Whole label is on one line, pull token end to closing `::` end. end_column = state.column + 1 end check_and_skip_token(state, "::") return init_ast_node({name, end_column = end_column}, loc, "Label") end local closing_tokens = utils.array_to_set({ "end", "eof", "else", "elseif", "until"}) statements["return"] = function(state, loc) if closing_tokens[state.token] or state.token == ";" then -- No return values. return init_ast_node({}, loc, "Return") else return init_ast_node(parse_expression_list(state), loc, "Return") end end statements["break"] = function(_, loc) return init_ast_node({}, loc, "Break") end statements["goto"] = function(state, loc) local name = check_name(state) skip_token(state) -- Skip label name. return init_ast_node({name}, loc, "Goto") end local function parse_expression_statement(state, loc) local lhs repeat local first_loc = lhs and location(state) or loc local expected = lhs and "identifier or field" or "statement" local primary_expression, in_parens = parse_simple_expression(state, expected, true) if in_parens then -- (expr) is invalid. parser.syntax_error(first_loc, first_loc.column, "expected " .. expected .. " near '('") end if primary_expression.tag == "Call" or primary_expression.tag == "Invoke" then if lhs then -- This is an assingment, and a call is not a valid lvalue. parse_error(state, "expected call or indexing") else -- It is a call. primary_expression.location = loc return primary_expression end end -- This is an assignment. lhs = lhs or {} lhs[#lhs+1] = primary_expression until not test_and_skip_token(state, ",") local equals_location = location(state) check_and_skip_token(state, "=") local rhs = parse_expression_list(state) return init_ast_node({lhs, rhs, equals_location = equals_location}, loc, "Set") end local function parse_statement(state) local loc = location(state) local statement_parser = statements[state.token] if statement_parser then skip_token(state) return statement_parser(state, loc) else return parse_expression_statement(state, loc) end end function parse_block(state, loc) local block = {location = loc} local after_statement = false while not closing_tokens[state.token] do local first_token = state.token if first_token == ";" then if not after_statement then table.insert(state.hanging_semicolons, location(state)) end skip_token(state) -- Do not allow several semicolons in a row, even if the first one is valid. after_statement = false else first_token = state.token_value or first_token local statement = parse_statement(state) after_statement = true statement.first_token = first_token block[#block+1] = statement if first_token == "return" then -- "return" must be the last statement. -- However, one ";" after it is allowed. test_and_skip_token(state, ";") if not closing_tokens[state.token] then parse_error(state, "expected end of block") end end end end return block end -- Parses source string. -- Returns AST (in almost MetaLua format), array of comments - tables {comment = string, location = location}, -- set of line numbers containing code, map of types of tokens wrapping line endings (nil, "string", or "comment"), -- and array of locations of empty statements (semicolons). -- On error throws {line = line, column = column, end_column = end_column, msg = msg} - an instance -- of parser.SyntaxError. function parser.parse(src) local state = new_state(src) skip_token(state) local ast = parse_block(state) check_token(state, "eof") return ast, state.comments, state.code_lines, state.line_endings, state.hanging_semicolons end return parser luacheck-0.21.1/src/luacheck/reachability.lua000066400000000000000000000033131315524664100210720ustar00rootroot00000000000000local core_utils = require "luacheck.core_utils" local reachability local function noop_callback() end local function reachability_callback(_, _, item, chstate, nested) if not item then return true end if not nested and item.lines then for _, subline in ipairs(item.lines) do reachability(chstate, subline, true) end end 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 not var.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_uninit(accessing_node, action_key == "mutations") end end end end end end end -- Emits warnings: unreachable code, uninitialized access. function reachability(chstate, line, nested) local reachable_indexes = {} core_utils.walk_line(line, reachable_indexes, 1, reachability_callback, chstate, nested) for i, item in ipairs(line.items) do if not reachable_indexes[i] then if item.location then chstate:warn_unreachable(item.location, item.loop_end, item.token) core_utils.walk_line(line, reachable_indexes, i, noop_callback) end end end end return reachability luacheck-0.21.1/src/luacheck/standards.lua000066400000000000000000000173341315524664100204250ustar00rootroot00000000000000local standards = {} -- A standard (aka std) defines set of allowed globals, their fields, -- and whether they are mutable. -- -- A standard can be in several formats. Internal (normalized) format -- is a tree. Each node defines a global or its field. Each node may have -- boolean `read_only` and `other_fields`, and may contain definitions -- of nested fields in `fields` subtable, which maps field names -- to definition tables. For example, standard defining globals -- of some Lua version may start like this: -- { -- -- Most globals are read-only by default. -- read_only = true, -- fields = { -- -- The tree can't be recursive, just allow everything for `_G`. -- _G = {other_fields = true, read_only = false}, -- package = { -- fields = { -- -- `other_fields` is false by default, so that an empty table -- -- defines a field that can't be indexed further (a function in this case). -- loadlib = {}, -- -- Allow doing everything with `package.loaded`. -- loaded = {other_fields = true, read_only = false}, -- -- More fields here... -- } -- }, -- -- More globals here... -- } -- } -- -- A similar format is used to define standards in table form -- in config. There are two differences: -- first, top level table can have two fields, `globals` and `read_globals`, -- that map global names to definition tables. Default value of `read_only` field -- for the these tables depends on which table they come from (`true` for `read_globals` -- and `false` for `globals`). Additionally, all tables that map field or global names -- to definition tables may have non-string keys, their associated values are interpreted -- as names instead and their definition table allows indexing with any keys indefinitely. -- E.g. `{fields = {"foo"}}` is equivalent to `{fields = {foo = {other_fields = true}}}`. -- This feature makes it easier to create less strict standards that do not care about fields, -- to ease migration from the old format. -- -- Additionally, there are some predefined named standards in `luacheck.builtin_standards` module. -- In config and inline options its possible to use their names as strings to refer to them. -- Validates an optional table mapping field names to field definitions or non-string keys to names. -- Returns a truthy value is the table is valid, a falsy value otherwise. local function validate_fields(fields) if fields == nil then return true end if type(fields) ~= "table" then return end for key, value in pairs(fields) do if type(key) == "string" then if type(value) ~= "table" then return end if value.read_only ~= nil and type(value.read_only) ~= "boolean" then return end if value.other_fields ~= nil and type(value.other_fields) ~= "boolean" then return end if not validate_fields(value.fields) then return end elseif type(value) ~= "string" then return end end return true end -- Validates an std table in user-side format. -- Returns a truthy value is the table is valid, a falsy value otherwise. function standards.validate_std_table(std_table) return type(std_table) == "table" and validate_fields(std_table.globals) and validate_fields(std_table.read_globals) end local infinitely_indexable_def = {other_fields = true} local function add_fields(def, fields, overwrite, ignore_array_part, default_read_only) if not fields then return end for field_name, field_def in pairs(fields) do if type(field_name) == "string" or not ignore_array_part then if type(field_name) ~= "string" then field_name = field_def field_def = infinitely_indexable_def end if not def.fields then def.fields = {} end if not def.fields[field_name] then def.fields[field_name] = {} end local existing_field_def = def.fields[field_name] local new_read_only = field_def.read_only if new_read_only == nil then new_read_only = default_read_only end if new_read_only ~= nil then if overwrite or new_read_only == false then existing_field_def.read_only = new_read_only end end if field_def.other_fields ~= nil then if overwrite or field_def.other_fields == true then existing_field_def.other_fields = field_def.other_fields end end add_fields(existing_field_def, field_def.fields, overwrite, false, nil) end end end -- Merges in an std table in user-side format. -- By default the new state of normalized std is a union of the standard tables being merged, -- e.g. if either table allows some field to be mutated, result should allow it, too. -- If `overwrite` is truthy, read-only statuses from the new std table overwrite existing values. -- If `ignore_top_array_part` is truthy, non-string keys in `globals` and `read_globals` tables -- in `std_table` are not processed. function standards.add_std_table(final_std, std_table, overwrite, ignore_top_array_part) add_fields(final_std, std_table.globals, overwrite, ignore_top_array_part, false) add_fields(final_std, std_table.read_globals, overwrite, ignore_top_array_part, true) end -- Overwrites or adds definition of a field with given read-only status and any nested keys. -- Field is specified as an array of field names. function standards.overwrite_field(final_std, field_names, read_only) local field_def = final_std for _, field_name in ipairs(field_names) do if not field_def.fields then field_def.fields = {} end if not field_def.fields[field_name] then field_def.fields[field_name] = {read_only = read_only} end field_def = field_def.fields[field_name] end for key in pairs(field_def) do field_def[key] = nil end field_def.read_only = read_only field_def.other_fields = true end -- Removes definition of a field from a normalized std table. -- Field is specified as an array of field names. function standards.remove_field(final_std, field_names) local field_def = final_std local parent_def for _, field_name in ipairs(field_names) do parent_def = field_def if not field_def.fields or not field_def.fields[field_name] then -- The field wasn't defined in the first place. return end field_def = field_def.fields[field_name] end if parent_def then parent_def.fields[field_names[#field_names]] = nil end end local function infer_deep_read_only_statuses(def, read_only) local deep_read_only = not def.other_fields or read_only if def.fields then for _, field_def in pairs(def.fields) do local field_read_only = read_only if field_def.read_only ~= nil then field_read_only = field_def.read_only end infer_deep_read_only_statuses(field_def, field_read_only) deep_read_only = deep_read_only and field_read_only and field_def.deep_read_only end end if deep_read_only then def.deep_read_only = true end end -- Finishes building a normalized std tables. -- Adds `deep_read_only` fields with `true` value to definition tables -- that do not have any writable fields, recursively. function standards.finalize(final_std) infer_deep_read_only_statuses(final_std, true) end local empty = {} -- Returns a definition table containing empty fields with given names. function standards.def_fields(...) local fields = {} for _, field in ipairs({...}) do fields[field] = empty end return {fields = fields} end return standards luacheck-0.21.1/src/luacheck/utils.lua000066400000000000000000000170671315524664100176050ustar00rootroot00000000000000local unpack = table.unpack or unpack -- luacheck: compat local utils = {} utils.dir_sep = package.config:sub(1,1) utils.is_windows = utils.dir_sep == "\\" local bom = "\239\187\191" -- Returns all contents of file (path or file handler) or nil + error message. function utils.read_file(file) local handler if type(file) == "string" then local open_err handler, open_err = io.open(file, "rb") if not handler then open_err = utils.unprefix(open_err, file .. ": ") return nil, "couldn't read: " .. open_err end else handler = file end local res, read_err = handler:read("*a") handler:close() if not res then return nil, "couldn't read: " .. read_err end -- Use :len() instead of # operator because in some environments -- string library is patched to handle UTF. if res:sub(1, bom:len()) == bom then res = res:sub(bom:len() + 1) end return res end -- luacheck: push -- luacheck: compat if _VERSION:find "5.1" then -- Loads Lua source string in an environment, returns function or nil, error. function utils.load(src, env, chunkname) local func, err = loadstring(src, chunkname) if func then if env then setfenv(func, env) end return func else return nil, err end end else -- Loads Lua source string in an environment, returns function or nil, error. function utils.load(src, env, chunkname) return load(src, chunkname, "t", env or _ENV) end end -- luacheck: pop -- Loads config containing assignments to global variables from path. -- Returns config table and return value of config or nil and error type -- ("I/O" or "syntax" or "runtime") and error message. function utils.load_config(path, env) env = env or {} local src, read_err = utils.read_file(path) if not src then return nil, "I/O", read_err end local func, load_err = utils.load(src, env, "chunk") if not func then return nil, "syntax", "line " .. utils.unprefix(load_err, "[string \"chunk\"]:") end local ok, res = pcall(func) if not ok then return nil, "runtime", "line " .. utils.unprefix(res, "[string \"chunk\"]:") end return env, res end function utils.array_to_set(array) local set = {} for index, value in ipairs(array) do set[value] = index end return set end function utils.concat_arrays(array) local res = {} for _, subarray in ipairs(array) do for _, item in ipairs(subarray) do table.insert(res, item) end end return res end function utils.update(t1, t2) for k, v in pairs(t2) do t1[k] = v end return t1 end function utils.remove(t1, t2) for k in pairs(t2) do t1[k] = nil end end local class_metatable = {} function class_metatable.__call(class, ...) local obj = setmetatable({}, class) if class.__init then class.__init(obj, ...) end return obj end function utils.class() local class = setmetatable({}, class_metatable) class.__index = class return class end function utils.is_instance(object, class) return rawequal(debug.getmetatable(object), class) end utils.Stack = utils.class() function utils.Stack:__init() self.size = 0 end function utils.Stack:push(value) self.size = self.size + 1 self[self.size] = value self.top = value end function utils.Stack:pop() local value = self[self.size] self[self.size] = nil self.size = self.size - 1 self.top = self[self.size] return value end local ErrorWrapper = utils.class() function ErrorWrapper:__init(err, traceback) self.err = err self.traceback = traceback end function ErrorWrapper:__tostring() return tostring(self.err) .. "\n" .. self.traceback end local function error_handler(err) if utils.is_instance(err, ErrorWrapper) then return err else return ErrorWrapper(err, debug.traceback()) end end -- Like pcall, but wraps errors in {err = err, traceback = traceback} -- tables unless already wrapped. function utils.try(f, ...) local args = {...} local num_args = select("#", ...) local function task() return f(unpack(args, 1, num_args)) end return xpcall(task, error_handler) end local function ripairs_iterator(array, i) if i == 1 then return nil else i = i - 1 return i, array[i] end end function utils.ripairs(array) return ripairs_iterator, array, #array + 1 end function utils.unprefix(str, prefix) if str:sub(1, #prefix) == prefix then return str:sub(#prefix + 1) else return str end end function utils.after(str, pattern) local _, last_matched_index = str:find(pattern) if last_matched_index then return str:sub(last_matched_index + 1) end end function utils.strip(str) local _, last_start_space = str:find("^%s*") local first_end_space = str:find("%s*$") return str:sub(last_start_space + 1, first_end_space - 1) end -- `sep` must be nil or a single character. Behaves like python's `str.split`. function utils.split(str, sep) local parts = {} local pattern if sep then pattern = sep .. "([^" .. sep .. "]*)" str = sep .. str else pattern = "%S+" end for part in str:gmatch(pattern) do table.insert(parts, part) end return parts end -- 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) self.err = err self.pattern = pattern end function utils.InvalidPatternError:__tostring() return self.err end -- Behaves like string.match, except it normally returns boolean and -- throws an instance of utils.InvalidPatternError on invalid pattern. -- The error object turns into original error when tostring is used on it, -- to ensure behaviour is predictable when luacheck is used as a module. function utils.pmatch(str, pattern) assert(type(str) == "string") assert(type(pattern) == "string") local ok, res = pcall(string.match, str, pattern) if not ok then error(utils.InvalidPatternError(res, pattern), 0) else return not not res end end -- Maps func over array. function utils.map(func, array) local res = {} for i, item in ipairs(array) do res[i] = func(item) end return res end -- Returns predicate checking type. function utils.has_type(type_) return function(x) return type(x) == type_ end end -- Returns predicate checking that value is an array with -- elements of type. function utils.array_of(type_) return function(x) if type(x) ~= "table" then return false end for _, item in ipairs(x) do if type(item) ~= type_ then return false end end return true end end -- Returns predicate chacking if value satisfies on of predicates. function utils.either(pred1, pred2) return function(x) return pred1(x) or pred2(x) end end return utils luacheck-0.21.1/src/luacheck/version.lua000066400000000000000000000012651315524664100201230ustar00rootroot00000000000000local luacheck = require "luacheck" local fs = require "luacheck.fs" local multithreading = require "luacheck.multithreading" local version = {} version.luacheck = luacheck._VERSION if rawget(_G, "jit") then version.lua = rawget(_G, "jit").version else version.lua = _VERSION end if fs.has_lfs then local lfs = require "lfs" version.lfs = lfs._VERSION else version.lfs = "Not found" end if multithreading.has_lanes then version.lanes = multithreading.lanes.ABOUT.version else version.lanes = "Not found" end version.string = ([[ Luacheck: %s Lua: %s LuaFileSystem: %s LuaLanes: %s]]):format(version.luacheck, version.lua, version.lfs, version.lanes) return version luacheck-0.21.1/src/luacheck/whitespace.lua000066400000000000000000000024361315524664100205730ustar00rootroot00000000000000local function check_whitespace(chstate, lines, line_endings) for line_number, line in ipairs(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 line_endings[line_number] then -- Trailing whitespace on code line or after long comment. code = "612" elseif line_endings[line_number] == "string" then -- Trailing whitespace embedded in a string literal. code = "613" elseif line_endings[line_number] == "comment" then -- Trailing whitespace at the end of a line comment or inside long comment. code = "614" end chstate:warn({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). chstate:warn({code = "621", line = line_number, column = from, end_column = to}) end end end end return check_whitespace