diff --git a/.github/Alipay.jpg b/.github/Alipay.jpg
deleted file mode 100644
index c76aa0fe7..000000000
Binary files a/.github/Alipay.jpg and /dev/null differ
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 07e672648..000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-custom: ["https://paypal.me/sumneko", "https://github.com/LuaLS/lua-language-server/issues/484"]
diff --git a/.github/WeChat Pay.jpg b/.github/WeChat Pay.jpg
deleted file mode 100644
index 32ce87223..000000000
Binary files a/.github/WeChat Pay.jpg and /dev/null differ
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 01b377ca1..fff4f6e4a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,40 +23,19 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - { os: ubuntu-22.04,   target: linux,   platform: linux-x64,   container: 'alpine:latest', libc: musl }
-          - { os: ubuntu-20.04,   target: linux,   platform: linux-x64    }
-          #- { os: ubuntu-20.04,   target: linux,   platform: linux-arm64  }
-          - { os: macos-11,       target: darwin,  platform: darwin-x64   }
-          - { os: macos-11,       target: darwin,  platform: darwin-arm64 }
-          - { os: windows-latest, target: windows, platform: win32-ia32   }
-          - { os: windows-latest, target: windows, platform: win32-x64    }
+          - { os: ubuntu-20.04,  target: linux,   platform: linux-x64, zip: x86_64-linux    }
+          - { os: macos-11,       target: darwin,  platform: darwin-x64, zip: x86_64-macos   }
+          - { os: macos-11,       target: darwin,  platform: darwin-arm64, zip: arm64-macos  }
+          - { os: windows-latest, target: windows, platform: win32-x64, zip: x86_64-win32    }
     runs-on: ${{ matrix.os }}
-    container:
-      image: ${{ matrix.container }}
     steps:
-      - name: Prepare container for musl
-        if: ${{ matrix.target == 'linux' && matrix.libc == 'musl' }}
-        run: |
-          apk update
-          apk add git ninja bash build-base nodejs linux-headers
-
       - uses: actions/checkout@v4
         with:
           submodules: recursive
+      - uses: actboy168/setup-luamake@master
+      - run: luamake -platform ${{ matrix.platform }}
 
-      - name: Build for others step-1
-        if: ${{ matrix.libc != 'musl' }}
-        uses: actboy168/setup-luamake@master
-
-      - name: Build for others step-2
-        if: ${{ matrix.libc != 'musl' }}
-        run: luamake -platform ${{ matrix.platform }}
-
-      - name: Build for musl
-        if: ${{ matrix.target == 'linux' && matrix.libc == 'musl' }}
-        run: ./make.sh
-
-      - name: Setting up workflow variables
+      - name: Create bin archive
         id: vars
         shell: bash
         run: |
@@ -67,24 +46,13 @@ jobs:
             PKG_VERSION=${GITHUB_SHA:0:7}
           fi
 
-          # Package suffix relative to the platform
-          if [[ "${{ matrix.target }}" = windows ]]; then
-            PKG_SUFFIX="zip"
-          else
-            PKG_SUFFIX="tar.gz"
-          fi
-
           # Package name w/ version
-          PKG_BASENAME="${{ env.PROJECT }}-${PKG_VERSION}-${{ matrix.platform }}"
-          if [[ "${{ matrix.libc }}" = musl ]]; then
-            PKG_BASENAME="${PKG_BASENAME}-${{matrix.libc}}"
-          fi
+          PKG_BASENAME="${{ matrix.zip }}"
 
           # Full name of the tarball asset
-          PKG_NAME="${PKG_BASENAME}.${PKG_SUFFIX}"
+          PKG_NAME="${PKG_BASENAME}.zip"
 
-          # Staging area for tarballs
-          PKG_STAGING="ci_staging/$PKG_BASENAME"
+          7z -y a ${PKG_NAME} ${{ env.BIN_DIR }} main.lua debugger.lua LICENSE locale meta/template/ meta/spell/ script
 
           echo PKG_VERSION=${PKG_VERSION}             >> $GITHUB_OUTPUT
           echo PKG_BASENAME=${PKG_BASENAME}           >> $GITHUB_OUTPUT
@@ -96,56 +64,50 @@ jobs:
         with:
           name: ${{ steps.vars.outputs.PKG_BASENAME }}
           path: |
-            ${{ env.BIN_DIR }}
-            main.lua
-            debugger.lua
-            LICENSE
-            changelog.md
-            locale
-            doc
-            meta
-            script
-
-      - name: Package tarballs
-        if: startsWith(github.ref, 'refs/tags/')
+            ${{ steps.vars.outputs.PKG_NAME }}
+  create-extension:
+    needs: compile
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/download-artifact@v4
+      - name: "Create "
         shell: bash
         run: |
-          STAGING=${{ steps.vars.outputs.PKG_STAGING }}
-          NAME=${{ steps.vars.outputs.PKG_NAME }}
-
-          # Making the staging area
-          mkdir -p ${STAGING}
-
-          # Copying binary and runtime files to staging area
-          cp -r main.lua debugger.lua LICENSE changelog.md locale meta script ${{ env.BIN_DIR }} ${STAGING}
-
-          # Creating release assets
-          pushd "${STAGING}/" >/dev/null
-          if [[ "${{ matrix.target }}" = windows ]]; then
-            7z -y a ${NAME} * | tail -2
-          else
-            tar czf ${NAME} *
-          fi
-          popd >/dev/null
-
-      # Packaging submodules for homebrew distribution
-      - name: Package submodules
-        id: submodules
-        if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.platform == 'darwin-x64' }}
-        run: |
-          STAGING=${{ steps.vars.outputs.PKG_STAGING }}
-          PKG_SUBMOD_NAME="${{ env.PROJECT }}-${{ steps.vars.outputs.PKG_VERSION }}-submodules.zip"
-          PKG_SUBMOD_PATH="${STAGING}/$PKG_SUBMOD_NAME"
-
-          zip -r $PKG_SUBMOD_PATH ./ -x "*.git*" -x "*.vscode*" -x "build*" -x "${{ env.BIN_DIR }}*" -x "${STAGING}*" -x "3rd/json.lua*" -x "log*" -x "ci_staging*"
-
-          echo PKG_SUBMOD_PATH=${PKG_SUBMOD_PATH} >> $GITHUB_OUTPUT
 
+          7z -y x arm64-macos/arm64-macos.zip -odefold/lsp-lua-language-server/plugins/bin/arm64-macos
+          cd defold/lsp-lua-language-server/plugins
+          7z -y a arm64-macos.zip bin
+          rm -r bin
+          cd -
+
+          7z -y x x86_64-macos/x86_64-macos.zip -odefold/lsp-lua-language-server/plugins/bin/x86_64-macos
+          cd defold/lsp-lua-language-server/plugins
+          7z -y a x86_64-macos.zip bin
+          rm -r bin 
+          cd -
+
+          7z -y x x86_64-win32/x86_64-win32.zip -odefold/lsp-lua-language-server/plugins/bin/x86_64-win32
+          cd defold/lsp-lua-language-server/plugins
+          7z -y a x86_64-win32.zip bin
+          rm -r bin 
+          cd -
+
+          7z -y x x86_64-linux/x86_64-linux.zip -odefold/lsp-lua-language-server/plugins/bin/x86_64-linux
+          cd defold/lsp-lua-language-server/plugins
+          7z -y a x86_64-linux.zip bin
+          rm -r bin 
+          cd -
+
+          cd defold
+          7z -y a release.zip *
+      - uses: actions/upload-artifact@v2
+        with:
+          name: release
+          path: defold/release.zip
       - name: Publish release assets
         uses: softprops/action-gh-release@v1
         if: startsWith(github.ref, 'refs/tags/')
         with:
           generate_release_notes: true
-          files: |
-            ${{ steps.vars.outputs.PKG_PATH }}
-            ${{ steps.submodules.outputs.PKG_SUBMOD_PATH }}
+          files: defold/release.zip
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index e8ff3632d..000000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: test
-on: [ push, pull_request ]
-jobs:
-  test:
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - { os: ubuntu-20.04,   platform: linux-x64 }
-          - { os: macos-14,       platform: darwin-arm64 }
-          - { os: windows-latest, platform: win32-x64 }
-    runs-on: ${{ matrix.os }}
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          submodules: recursive
-      - uses: actboy168/setup-luamake@master
-      - run: luamake -platform ${{ matrix.platform }}
diff --git a/.gitmodules b/.gitmodules
index 32d827327..23796c526 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,6 +16,9 @@
 [submodule "3rd/EmmyLuaCodeStyle"]
 	path = 3rd/EmmyLuaCodeStyle
 	url = https://github.com/CppCXY/EmmyLuaCodeStyle
+[submodule "3rd/luafilesystem"]
+	path = 3rd/luafilesystem
+	url = https://github.com/lunarmodules/luafilesystem
 [submodule "3rd/json.lua"]
 	path = 3rd/json.lua
 	url = https://github.com/actboy168/json.lua
diff --git a/3rd/luafilesystem b/3rd/luafilesystem
new file mode 160000
index 000000000..7c6e1b013
--- /dev/null
+++ b/3rd/luafilesystem
@@ -0,0 +1 @@
+Subproject commit 7c6e1b013caec0602ca4796df3b1d7253a2dd258
diff --git a/README.md b/README.md
index aa40ab379..a1ef76524 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,29 @@
+# [Defold](https://defold.com/)'s fork of lua language server
+
+This language server works from the Defold editor and can be used to lint Defold projects. Learn more: https://forum.defold.com/t/linting-in-the-code-editor/72465
+
+## Notable changes
+
+1. Add build a defold library project and a build script that assembles it on [release](https://github.com/defold/lua-language-server/releases) — use `release.zip` links as dependencies.
+2. Add text document and workspace diagnostics.
+3. Use LuaCheck for diagnostics.
+
+## Known issues
+
+EmmyLua might [trip over the source code](https://github.com/EmmyLua/VSCode-EmmyLua/issues/123) of this project when it's extracted by the Defold editor to `/build/plugins`. The solution is to configure EmmyLua to ignore the build folder with this `emmy.config.json` file:
+```json
+{
+    "source": [
+        {
+            "dir": ".",
+            "exclude": [
+                "build/**"
+            ]
+        }
+    ]
+}
+```
+
 # lua-language-server
 
 ![build](https://img.shields.io/github/actions/workflow/status/LuaLS/lua-language-server/.github%2Fworkflows%2Fbuild.yml)
diff --git a/changelog.md b/changelog.md
index 8f19fd0a4..8ac8e6519 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,7 @@
 # changelog
 
+## Unreleased
+* `NEW` Added LuaCheck
 ## 3.8.3
 `2024-4-23`
 * `FIX` server may crash when the workspace is using a non-English path.
diff --git a/defold/game.project b/defold/game.project
new file mode 100644
index 000000000..5e92e9641
--- /dev/null
+++ b/defold/game.project
@@ -0,0 +1,3 @@
+[library]
+include_dirs = lsp-lua-language-server
+
diff --git a/defold/lsp-lua-language-server/ext.manifest b/defold/lsp-lua-language-server/ext.manifest
new file mode 100644
index 000000000..5dbd6791c
--- /dev/null
+++ b/defold/lsp-lua-language-server/ext.manifest
@@ -0,0 +1 @@
+name: "lsp-lua-language-server"
\ No newline at end of file
diff --git a/defold/lsp-lua-language-server/lsp.editor_script b/defold/lsp-lua-language-server/lsp.editor_script
new file mode 100644
index 000000000..d1f7c27db
--- /dev/null
+++ b/defold/lsp-lua-language-server/lsp.editor_script
@@ -0,0 +1,20 @@
+local M = {}
+
+function M.get_language_servers()
+    local cmd = 'build/plugins/lsp-lua-language-server/plugins/bin/' .. editor.platform .. '/bin/lua-language-server'
+    if editor.platform == 'x86_64-win32' then
+        cmd = cmd .. '.exe'
+    end
+    local config_arg = '--configpath=build/plugins/lsp-lua-language-server/plugins/share/config.json'
+    return {
+        {
+            languages = {'lua'},
+            watched_files = {
+                { pattern = '**/.luacheckrc' }
+            },
+            command = { cmd, config_arg }
+        }
+    }
+end
+
+return M
diff --git a/defold/lsp-lua-language-server/plugins/share/config.json b/defold/lsp-lua-language-server/plugins/share/config.json
new file mode 100644
index 000000000..975b8f294
--- /dev/null
+++ b/defold/lsp-lua-language-server/plugins/share/config.json
@@ -0,0 +1,16 @@
+{
+    "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
+    "diagnostics": {
+        "workspaceEvent": "None",
+        "enable": true
+    },
+    "Lua": {
+        "runtime": {
+            "version": "Lua 5.1"
+        }
+    },
+    "window": {
+        "progressBar": false,
+        "statusBar": false
+    }
+}
\ No newline at end of file
diff --git a/make.lua b/make.lua
index a0dcb4513..4fd326eb1 100644
--- a/make.lua
+++ b/make.lua
@@ -1,5 +1,7 @@
 local lm = require 'luamake'
 
+lm.notest = true
+-- lm.bindir = "bin"
 lm.c = lm.compiler == 'msvc' and 'c89' or 'c11'
 lm.cxx = 'c++17'
 
@@ -30,12 +32,26 @@ lm:source_set 'lpeglabel' {
     },
 }
 
+lm:source_set 'luafilesystem' {
+    rootdir = '3rd',
+    includes = "bee.lua/3rd/lua",
+    sources = "luafilesystem/src/*.c",
+    linux = {
+        flags = "-g -Wall -Werror=format",
+        defines = {
+            "__STDC_LIMIT_MACROS",
+            "_BSD_SOURCE"
+        }
+    }
+}
+
 lm:executable "lua-language-server" {
     deps = {
         "source_bee",
         "source_lua",
         "source_bootstrap",
         "lpeglabel",
+        "luafilesystem",
         includeCodeFormat and "code_format" or nil,
     },
     includes = {
diff --git a/make/modules.cpp b/make/modules.cpp
index 1dd4a3704..1417aef4e 100644
--- a/make/modules.cpp
+++ b/make/modules.cpp
@@ -3,6 +3,9 @@
 extern "C" int luaopen_lpeglabel (lua_State *L);
 static ::bee::lua::callfunc _init(::bee::lua::register_module, "lpeglabel", luaopen_lpeglabel);
 
+extern "C" int luaopen_lfs (lua_State *L);
+static ::bee::lua::callfunc _init_lfs(::bee::lua::register_module, "lfs", luaopen_lfs);
+
 #ifdef CODE_FORMAT
 extern "C" int luaopen_code_format(lua_State *L);
 static ::bee::lua::callfunc _init_code_format(::bee::lua::register_module, "code_format",
diff --git a/new_release_tag.sh b/new_release_tag.sh
new file mode 100755
index 000000000..7e5a84469
--- /dev/null
+++ b/new_release_tag.sh
@@ -0,0 +1,5 @@
+#/usr/bin/env bash
+set -euox pipefail
+VERSION="v1.$(git rev-list --count HEAD)"
+git tag "$VERSION"
+git push origin "$VERSION"
\ No newline at end of file
diff --git a/script/core/diagnostics/luacheck.lua b/script/core/diagnostics/luacheck.lua
new file mode 100644
index 000000000..10d88d2da
--- /dev/null
+++ b/script/core/diagnostics/luacheck.lua
@@ -0,0 +1,83 @@
+local files   = require 'files'
+local define  = require 'proto.define'
+local log     = require 'log'
+local lspconfig = require 'config'
+local luacheck = require "luacheck.check"
+local lcformat = require "luacheck.format"
+local lcfilter = require "luacheck.filter"
+local lcconfig = require "luacheck.config"
+local workspace = require "workspace"
+local util     = require 'utility'
+local fs       = require "luacheck.fs"
+local inspect    = require 'inspect'
+local platform = require 'bee.platform'
+
+
+local config_stack = nil
+
+-- a bit hacky but this will monkey patch the isLua function so that
+-- we can know when the .luacheckrc file has been changed
+-- a call to isLua is done in workspace.lua fw.event() on change
+-- we could also add .luacheck as a recognised file association using
+-- a config file (see config.template and the files.associations value)
+-- but this works equally well for our purposes
+local isLua = files.isLua
+files.isLua = function(uri)
+    if util.stringEndWith(uri:lower(), '.luacheckrc') then
+        config_stack = nil
+        return false
+    end
+    return isLua(uri)
+end
+
+return function (uri, callback)
+
+    if not config_stack then
+        log.info("loading .luacheckrc")
+        local rootUri = (workspace.rootUri or ""):gsub(platform.os == "windows" and "file:///" or "file://", "")
+        local path = fs.join(rootUri, ".luacheckrc")
+        if platform.os == 'windows' then
+            path = path:gsub('/', "\\")
+        end
+
+        local global_path = nil
+        local config = lcconfig.load_config(path, global_path) or {options={stds={},files={},globals={}}}
+
+        local editor_defaults = {
+            options = {
+                unused_args = false,
+                max_line_length = false,
+                ignore = {
+                    "611", -- line contains only whitespace
+                    "612", -- line contains trailing whitespace
+                    "614", -- trailing whitespace in a comment
+                },
+                stds = {},
+                files = {},
+                globals = lspconfig.get(workspace.rootUri, 'Lua.diagnostics.globals') or {} -- use editor-provided globals
+            }
+        }
+
+        -- create final config stack combining LuaCheck config, Defold globals and editor globals
+        config_stack = lcconfig.stack_configs({ editor_defaults, config })
+    end
+
+    local text = files.getText(uri)
+    local report = luacheck(text)
+    local reports = { report }
+    local opts = { config_stack:get_options(uri) }
+    local stds = config_stack:get_stds()
+    lcfilter.filter(reports, opts, stds)
+
+    for i,warning in ipairs(report.filtered_warnings) do
+        local message = lcformat.get_message(warning)
+        local start = ((warning.line - 1) * 10000) + warning.column - 1
+        local finish = ((warning.line - 1) * 10000) + warning.end_column
+        callback({
+            start   = start,
+            finish  = finish,
+            tags    = { define.DiagnosticTag.Unnecessary },
+            message = message,
+        })
+    end
+end
diff --git a/script/luacheck/builtin_standards/init.lua b/script/luacheck/builtin_standards/init.lua
new file mode 100755
index 000000000..9805b4ac6
--- /dev/null
+++ b/script/luacheck/builtin_standards/init.lua
@@ -0,0 +1,301 @@
+local love = require "luacheck.builtin_standards.love"
+local ngx = require "luacheck.builtin_standards.ngx"
+local 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, ngx)
+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 get_running_lua_std_name()
+   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[get_running_lua_std_name()]
+
+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", "randomize"
+   }
+}
+
+builtin_standards.love = love
+
+builtin_standards.rockspec = {
+   globals = {
+      "rockspec_format", "package", "version", "description", "dependencies", "supported_platforms",
+      "external_dependencies", "source", "build", "hooks", "deploy", "build_dependencies", "test_dependencies", "test"
+   }
+}
+
+builtin_standards.luacheckrc = {
+   globals = {
+      "global", "unused", "redefined", "unused_args", "unused_secondaries", "self", "compat", "allow_defined",
+      "allow_defined_top", "module", "globals", "read_globals", "new_globals", "new_read_globals", "not_globals",
+      "ignore", "enable", "only", "std", "max_line_length", "max_code_line_length", "max_string_line_length",
+      "max_comment_line_length", "max_cyclomatic_complexity", "quiet", "color", "codes", "ranges", "formatter",
+      "cache", "jobs", "files", "stds", "exclude_files", "include_files"
+   }
+}
+
+builtin_standards.none = {}
+
+return builtin_standards
diff --git a/script/luacheck/builtin_standards/love.lua b/script/luacheck/builtin_standards/love.lua
new file mode 100755
index 000000000..aa173dc53
--- /dev/null
+++ b/script/luacheck/builtin_standards/love.lua
@@ -0,0 +1,137 @@
+local 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,
+      errorhandler = read_write,
+      filedropped = read_write,
+      focus = read_write,
+      gamepadaxis = read_write,
+      gamepadpressed = read_write,
+      gamepadreleased = read_write,
+      handlers = read_write,
+      hasDeprecationOutput = empty,
+      joystickadded = read_write,
+      joystickaxis = read_write,
+      joystickhat = read_write,
+      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,
+      setDeprecationOutput = empty,
+      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","setDistanceModel","setDopplerScale",
+         "setOrientation","setPosition","setVelocity", "setVolume","stop","getActiveSourceCount","getRecordingDevices",
+         "newQueueableSource","setEffect","getActiveEffects","getEffect","getMaxSceneEffects","getMaxSourceEffects",
+         "isEffectsSupported","setMixWithSystem"),
+
+      data = standards.def_fields("compress","decode","decompress","encode","getPackedSize","hash","newByteData",
+         "newDataView","pack","unpack"),
+
+      event = standards.def_fields("clear","poll","pump","push","quit","wait"),
+
+      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",
+         "getInfo","setCRequirePath","getCRequirePath"),
+
+      font = standards.def_fields("newImageRasterizer", "newGlyphData", "newTrueTypeRasterizer",
+         "newBMFontRasterizer", "newRasterizer"),
+
+      graphics = standards.def_fields("arc","circle","clear","discard","draw","ellipse","getBackgroundColor",
+         "getBlendMode","getCanvas","getCanvasFormats","getColor","getColorMask",
+         "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",
+         "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",
+         "captureScreenshot","getImageFormats","newArrayImage","newVolumeImage","newCubeImage",
+         "getTextureTypes","drawLayer","setDepthMode","setMeshCullMode","setFrontFaceWinding",
+         "applyTransform","replaceTransform","transformPoint","inverseTransformPoint","getStackDepth",
+         "flushBatch","validateShader","drawInstanced","getDepthMode","getFrontFaceWinding","getMeshCullMode",
+         "getDPIScale","getPixelDimensions","getPixelHeight","getPixelWidth","isActive","getDefaultMipmapFilter",
+         "setDefaultMipmapFilter"),
+
+      image = standards.def_fields("isCompressed","newCompressedData","newImageData","newCubeFaces"),
+
+      joystick = standards.def_fields("getJoystickCount","getJoysticks","loadGamepadMappings",
+         "saveGamepadMappings","setGamepadMapping"),
+
+      keyboard = standards.def_fields("getKeyFromScancode","getScancodeFromKey","hasKeyRepeat","hasTextInput",
+         "isDown","isScancodeDown","setKeyRepeat","setTextInput","hasScreenKeyboard"),
+
+      math = standards.def_fields("compress","decompress","gammaToLinear","getRandomSeed","getRandomState",
+         "isConvex","linearToGamma","newBezierCurve","newRandomGenerator","noise","random",
+         "randomNormal","setRandomSeed","setRandomState","triangulate","newTransform"),
+
+      mouse = standards.def_fields("getCursor","getPosition","getRelativeMode","getSystemCursor","getX",
+         "getY","hasCursor","isDown","isGrabbed","isVisible","newCursor","setCursor","setGrabbed",
+         "setPosition","setRelativeMode","setVisible","setX","setY","isCursorSupported"),
+
+      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","hasBackgroundMusic"),
+
+      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",
+         "updateMode","isMinimized","restore","getDPIScale")
+   }
+}
+
+-- `love` standard contains only `love` global, so return it here directly using normal std format.
+return {
+   read_globals = {love = love}
+}
diff --git a/script/luacheck/builtin_standards/ngx.lua b/script/luacheck/builtin_standards/ngx.lua
new file mode 100755
index 000000000..a1b44fce6
--- /dev/null
+++ b/script/luacheck/builtin_standards/ngx.lua
@@ -0,0 +1,152 @@
+local 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
diff --git a/script/luacheck/cache.lua b/script/luacheck/cache.lua
new file mode 100755
index 000000000..fe26fe9ac
--- /dev/null
+++ b/script/luacheck/cache.lua
@@ -0,0 +1,139 @@
+local fs = require "luacheck.fs"
+local serializer = require "luacheck.serializer"
+local sha1 = require "luacheck.vendor.sha1"
+local utils = require "luacheck.utils"
+
+local cache = {}
+
+-- Check results can be cached inside a given cache directory.
+-- Check result for a file is stored in `<cache_dir>/<SHA1(filename)>`.
+-- Cache file format: <format_version>\n<filename>\n<serialized check result`.
+
+-- Returns default cache directory or nothing.
+function cache.get_default_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", "Cache")
+      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", "Caches", "Luacheck")
+         end
+      else
+         local config_home_dir = os.getenv("XDG_CACHE_HOME")
+
+         if not config_home_dir then
+            local home_dir = os.getenv("HOME")
+
+            if home_dir then
+               config_home_dir = fs.join(home_dir, ".cache")
+            end
+         end
+
+         if config_home_dir then
+            return fs.join(config_home_dir, "luacheck")
+         end
+      end
+   end
+end
+
+local format_version = "1"
+
+local Cache = utils.class()
+
+function Cache:__init(cache_directory)
+   local ok, err = fs.make_dirs(cache_directory)
+
+   if not ok then
+      return nil, ("Couldn't initialize cache in %s: %s"):format(cache_directory, err)
+   end
+
+   self._dir = cache_directory
+   self._current_dir = fs.get_current_dir()
+end
+
+-- Caches check result for a file. Returns true on success, nothing on error.
+function Cache:put(filename, check_result)
+   local normalized_filename = fs.normalize(fs.join(self._current_dir, filename))
+   local cache_filename = fs.join(self._dir, sha1.sha1(normalized_filename))
+
+   local fh = io.open(cache_filename, "wb")
+
+   if not fh then
+      return
+   end
+
+   local serialized_result = serializer.dump_check_result(check_result)
+   fh:write(format_version, "\n", normalized_filename, "\n", serialized_result)
+   fh:close()
+   return true
+end
+
+-- Retrieves cached check result for a file.
+-- Returns check result on cache hit, nothing on cache miss,
+-- nil and true on malformed cache data.
+function Cache:get(filename)
+   local normalized_filename = fs.normalize(fs.join(self._current_dir, filename))
+   local cache_filename = fs.join(self._dir, sha1.sha1(normalized_filename))
+
+   local file_mtime = fs.get_mtime(filename)
+   local cache_mtime = fs.get_mtime(cache_filename)
+
+   if not file_mtime or not cache_mtime or file_mtime >= cache_mtime then
+      return
+   end
+
+   local fh = io.open(cache_filename, "rb")
+
+   if not fh then
+      return
+   end
+
+   if fh:read() ~= format_version then
+      fh:close()
+      return
+   end
+
+   if fh:read() ~= normalized_filename then
+      fh:close()
+      return
+   end
+
+   local serialized_result = fh:read("*a")
+   fh:close()
+
+   if not serialized_result then
+      return nil, true
+   end
+
+   local result = serializer.load_check_result(serialized_result)
+
+   if not result then
+      return nil, true
+   end
+
+   return result
+end
+
+function cache.new(cache_directory)
+   return Cache(cache_directory)
+end
+
+return cache
diff --git a/script/luacheck/check.lua b/script/luacheck/check.lua
new file mode 100755
index 000000000..a995bf394
--- /dev/null
+++ b/script/luacheck/check.lua
@@ -0,0 +1,96 @@
+local check_state = require "luacheck.check_state"
+local core_utils = require "luacheck.core_utils"
+local parse_inline_options = require "luacheck.stages.parse_inline_options"
+local parser = require "luacheck.parser"
+local stages = require "luacheck.stages"
+local utils = require "luacheck.utils"
+
+local inline_option_fields = utils.array_to_set(parse_inline_options.inline_option_fields)
+
+local function validate_fields(tables, per_code_fields)
+   for _, t in ipairs(tables) do
+      local fields_set
+
+      if per_code_fields then
+         if not t.code then
+            error("Warning has no code", 0)
+         end
+
+         local warning_info = stages.warnings[t.code]
+
+         if not warning_info then
+            error("Unknown issue code " .. t.code, 0)
+         end
+
+         fields_set = warning_info.fields_set
+      else
+         fields_set = inline_option_fields
+      end
+
+      for field in pairs(t) do
+         if not fields_set[field] then
+            error("Unknown field " .. field .. " in " ..
+               (per_code_fields and "issue with code " .. t.code or "inline option table"), 0)
+         end
+      end
+   end
+end
+
+--- 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.
+--    `line_lengths`: map from line numbers to line lengths.
+--    `line_endings`: map from line numbers to "comment", "string", or `nil` base on
+--                    whether the line ending is within a token.
+-- If `events` array contains a syntax error, the other fields are empty tables.
+local function check(source)
+   local chstate = check_state.new(source)
+   local ok, error_wrapper = utils.try(stages.run, chstate)
+   local warnings, inline_options, line_lengths, line_endings
+
+   if ok then
+      warnings = chstate.warnings
+      core_utils.sort_by_location(warnings)
+      inline_options = chstate.inline_options
+      line_lengths = chstate.line_lengths
+      line_endings = chstate.line_endings
+   else
+      local err = error_wrapper.err
+
+      if not utils.is_instance(err, parser.SyntaxError) then
+         error(error_wrapper, 0)
+      end
+
+      local syntax_error = {
+         code = "011",
+         line = err.line,
+         column = chstate:offset_to_column(err.line, err.offset),
+         end_column = chstate:offset_to_column(err.line, err.end_offset),
+         msg = err.msg
+      }
+
+      if err.prev_line then
+         syntax_error.prev_line = err.prev_line
+         syntax_error.prev_column = chstate:offset_to_column(err.prev_line, err.prev_offset)
+         syntax_error.prev_end_column = chstate:offset_to_column(err.prev_line, err.prev_end_offset)
+      end
+
+      warnings = {syntax_error}
+      inline_options = {}
+      line_lengths = {}
+      line_endings = {}
+   end
+
+   validate_fields(warnings, true)
+   validate_fields(inline_options)
+
+   return {
+      warnings = warnings,
+      inline_options = inline_options,
+      line_lengths = line_lengths,
+      line_endings = line_endings
+   }
+end
+
+return check
diff --git a/script/luacheck/check_state.lua b/script/luacheck/check_state.lua
new file mode 100755
index 000000000..42cc0ac15
--- /dev/null
+++ b/script/luacheck/check_state.lua
@@ -0,0 +1,66 @@
+local utils = require "luacheck.utils"
+
+local check_state = {}
+
+local CheckState = utils.class()
+
+function CheckState:__init(source_bytes)
+   self.source_bytes = source_bytes
+   self.warnings = {}
+end
+
+-- Returns column of a character in a line given its offset.
+-- The column is never larger than the line length.
+-- This can be called if line length is not yet known.
+function CheckState:offset_to_column(line, offset)
+   local line_length = self.line_lengths[line]
+   local column = offset - self.line_offsets[line] + 1
+
+   if not line_length then
+      return column
+   end
+
+   return math.max(1, math.min(line_length, column))
+end
+
+function CheckState:warn_column_range(code, range, warning)
+   warning = warning or {}
+   warning.code = code
+   warning.line = range.line
+   warning.column = range.column
+   warning.end_column = range.end_column
+   table.insert(self.warnings, warning)
+   return warning
+end
+
+function CheckState:warn(code, line, offset, end_offset, warning)
+   warning = warning or {}
+   warning.code = code
+   warning.line = line
+   warning.column = self:offset_to_column(line, offset)
+   warning.end_column = self:offset_to_column(line, end_offset)
+   table.insert(self.warnings, warning)
+   return warning
+end
+
+function CheckState:warn_range(code, range, warning)
+   return self:warn(code, range.line, range.offset, range.end_offset, warning)
+end
+
+function CheckState:warn_var(code, var, warning)
+   warning = self:warn_range(code, var.node, warning)
+   warning.name = var.name
+   return warning
+end
+
+function CheckState:warn_value(code, value, warning)
+   warning = self:warn_range(code, value.var_node, warning)
+   warning.name = value.var.name
+   return warning
+end
+
+function check_state.new(source_bytes)
+   return CheckState(source_bytes)
+end
+
+return check_state
diff --git a/script/luacheck/config.lua b/script/luacheck/config.lua
new file mode 100755
index 000000000..d31143ce8
--- /dev/null
+++ b/script/luacheck/config.lua
@@ -0,0 +1,542 @@
+local cache = require "luacheck.cache"
+local options = require "luacheck.options"
+local builtin_standards = require "luacheck.builtin_standards"
+local fs = require "luacheck.fs"
+local globbing = require "luacheck.globbing"
+local standards = require "luacheck.standards"
+local utils = require "luacheck.utils"
+
+local config = {}
+
+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
+
+-- A single config is represented by a table with fields:
+-- * `options`: table with all config scope options, including `stds` and `files`.
+-- * `config_path`: optional path to file from which config was loaded, used only in error messages.
+-- * `anchor_dir`: absolute path to directory relative to which config was loaded,
+--   or nil if the config is not anchored. Paths within a config are adjusted to be absolute
+--   relative to anchor directory, or current directory if it's not anchored.
+--   As current directory can change between config usages, this adjustment happens on demand.
+
+-- Returns config path and optional anchor directory or nil and optional error message.
+local function locate_config(path, global_path)
+   if path == false then
+      return
+   end
+
+   local is_default_path = not path
+   path = path or config.default_path
+
+   if fs.is_absolute(path) then
+      return path
+   end
+
+   local current_dir = fs.get_current_dir()
+   local anchor_dir, rel_dir = fs.find_file(current_dir, path)
+
+   if anchor_dir then
+      return fs.join(rel_dir, path), anchor_dir
+   end
+
+   if not is_default_path then
+      return nil, ("Couldn't find configuration file %s"):format(path)
+   end
+
+   if global_path == false then
+      return
+   end
+
+   global_path = global_path or config.get_default_global_path()
+
+   if global_path and fs.is_file(global_path) then
+      return global_path, (fs.split_base(global_path))
+   end
+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(anchor_dir)
+   if not anchor_dir then
+      return
+   end
+
+   local function loader(modname)
+      local modpath = fs.join(anchor_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)
+   if not loader then
+      return
+   end
+
+   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
+
+-- Requires module from config anchor directory.
+-- Returns success flag and module or error message.
+function config.relative_require(anchor_dir, modname)
+   local loader = add_relative_loader(anchor_dir)
+   local ok, mod_or_err = pcall(require, modname)
+   remove_relative_loader(loader)
+   return ok, mod_or_err
+end
+
+-- 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 function set_default_std(files, pattern, std)
+   -- Avoid mutating option tables, they may be shared between different patterns.
+   local pattern_opts = {std = std}
+
+   if files[pattern] then
+      pattern_opts = utils.update(pattern_opts, files[pattern])
+   end
+
+   files[pattern] = pattern_opts
+end
+
+local function add_default_path_options(opts)
+   local files = {}
+
+   if opts.files then
+      files = utils.update(files, opts.files)
+   end
+
+   opts.files = files
+   set_default_std(files, "**/spec/**/*_spec.lua", "+busted")
+   set_default_std(files, "**/test/**/*_spec.lua", "+busted")
+   set_default_std(files, "**/tests/**/*_spec.lua", "+busted")
+   set_default_std(files, "**/*.rockspec", "+rockspec")
+   set_default_std(files, "**/*.luacheckrc", "+luacheckrc")
+end
+
+local fallback_config = {options = {}, anchor_dir = ""}
+add_default_path_options(fallback_config.options)
+
+-- Loads config from a file, if possible.
+-- `path` and `global_path` can be nil (will use default), false (will disable loading), or a string.
+-- Doesn't validate the config.
+-- Returns a table or nil and an error message.
+function config.load_config(path, global_path)
+   local config_path, anchor_dir = locate_config(path, global_path)
+
+   if not config_path then
+      if anchor_dir then
+         return nil, anchor_dir
+      else
+         return fallback_config
+      end
+   end
+
+   local env, special_values = make_config_env()
+   local loader = add_relative_loader(anchor_dir)
+   local load_ok, ret, load_err = utils.load_config(config_path, env)
+   remove_relative_loader(loader)
+
+   if not load_ok then
+      return nil, ("Couldn't load configuration from %s: %s error (%s)"):format(config_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)
+   add_default_path_options(env)
+   return {options = env, config_path = config_path, anchor_dir = anchor_dir}
+end
+
+function config.table_to_config(opts)
+   return {options = opts}
+end
+
+-- Validates custom stds within a config table and adds them to stds map.
+-- Returns true on success or nil and an error message on error.
+local function add_stds_from_config(conf, stds)
+   if conf.options.stds ~= nil then
+      if type(conf.options.stds) ~= "table" then
+         return nil, ("invalid option 'stds': table expected, got %s"):format(type(conf.options.stds))
+      end
+
+      -- Validate stds in sorted order for deterministic output when more than one std is invalid.
+      local std_names = {}
+
+      for std_name in pairs(conf.options.stds) do
+         if type(std_name) == "string" then
+            table.insert(std_names, std_name)
+         end
+      end
+
+      table.sort(std_names)
+
+      for _, std_name in ipairs(std_names) do
+         local std = conf.options.stds[std_name]
+
+         if type(std) ~= "table" then
+            return nil, ("invalid custom std '%s': table expected, got %s"):format(std_name, type(std))
+         end
+
+         local ok, err = standards.validate_std_table(std)
+
+         if not ok then
+            return nil, ("invalid custom std '%s': %s"):format(std_name, err)
+         end
+
+         stds[std_name] = std
+      end
+   end
+
+   return true
+end
+
+local function error_prefix(conf)
+   if conf.config_path then
+      return ("in config loaded from %s: "):format(conf.config_path)
+   else
+      return ""
+   end
+end
+
+local function quiet_validator(x)
+   if type(x) == "number" then
+      if math.floor(x) == x and x >= 0 and x <= 3 then
+         return true
+      else
+         return false, ("integer in range 0..3 expected, got %.20g"):format(x)
+      end
+   else
+      return false, ("integer in range 0..3 expected, got %s"):format(type(x))
+   end
+end
+
+local function jobs_validator(x)
+   if type(x) == "number" then
+      if math.floor(x) == x and x >= 1 then
+         return true
+      else
+         return false, ("positive integer expected, got %.20g"):format(x)
+      end
+   else
+      return false, ("positive integer expected, got %s"):format(type(x))
+   end
+end
+
+config.format_options = {
+   quiet = quiet_validator,
+   color = utils.has_type("boolean"),
+   codes = utils.has_type("boolean"),
+   ranges = utils.has_type("boolean"),
+   formatter = utils.has_either_type("string", "function")
+}
+
+local top_options = {
+   cache = utils.has_either_type("string", "boolean"),
+   jobs = jobs_validator,
+   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, config.format_options)
+utils.update(top_options, options.all_options)
+
+-- Returns true if config is valid, nil and error message otherwise.
+local function validate_config(conf, stds)
+   local ok, err = options.validate(top_options, conf.options, stds)
+
+   if not ok then
+      return nil, err
+   end
+
+   if conf.options.files then
+      for path, opts in pairs(conf.options.files) do
+         if type(path) == "string" then
+            ok, err = options.validate(options.all_options, opts, stds)
+
+            if not ok then
+               return nil, ("invalid options for path '%s': %s"):format(path, err)
+            end
+         end
+      end
+   end
+
+   return true
+end
+
+local ConfigStack = utils.class()
+
+function ConfigStack:__init(configs, stds)
+   self._configs = configs
+   self._stds = stds
+end
+
+function ConfigStack:get_stds()
+   return self._stds
+end
+
+-- Accepts an array of config tables, as returned from `load_config` and `table_to_config`.
+-- Assumes that configs closer to end of the array override configs closer to beginning.
+-- Returns an instance of `ConfigStack`. On validation error returns nil and an error message.
+function config.stack_configs(configs)
+   -- First, collect and validate stds from all configs, they are required to validate `std` option.
+   local stds = utils.update({}, builtin_standards)
+
+   for _, conf in ipairs(configs) do
+      local ok, err = add_stds_from_config(conf, stds)
+
+      if not ok then
+         return nil, error_prefix(conf) .. err
+      end
+   end
+
+   for _, conf in ipairs(configs) do
+      local ok, err = validate_config(conf, stds)
+
+      if not ok then
+         return nil, error_prefix(conf) .. err
+      end
+   end
+
+   return ConfigStack(configs, stds)
+end
+
+-- Returns a table of top-level config options, except `files` and `stds`.
+function ConfigStack:get_top_options()
+   local res = {
+      quiet = 0,
+      color = true,
+      codes = false,
+      ranges = false,
+      formatter = "default",
+      cache = false,
+      jobs = false,
+      include_files = {},
+      exclude_files = {}
+   }
+
+   local current_dir = fs.get_current_dir()
+   local last_anchor_dir
+
+   for _, conf in ipairs(self._configs) do
+      for _, option in ipairs({"quiet", "color", "codes", "ranges", "jobs"}) do
+         if conf.options[option] ~= nil then
+            res[option] = conf.options[option]
+         end
+      end
+
+      -- It's not immediately obvious relatively to which config formatter modules
+      -- should be resolved when they are specified in a config without an anchor dir.
+      -- For now, use the last anchor directory available, that should result
+      -- in reasonable behaviour in the current case of a single anchored config (loaded from file)
+      -- + a single not anchored config (loaded from CLI options).
+      last_anchor_dir = conf.anchor_dir or last_anchor_dir
+
+      if conf.options.formatter ~= nil then
+         res.formatter = conf.options.formatter
+         res.formatter_anchor_dir = last_anchor_dir
+      end
+
+      -- Path options, on the other hand, are interpreted relatively to the current directory
+      -- when specified in a config without anchor. Behaviour similar to formatter could also
+      -- make sense, but this is consistent with pre 0.22.0 behaviou
+      local anchor_dir = conf.anchor_dir or current_dir
+
+      for _, option in ipairs({"include_files", "exclude_files"}) do
+         if conf.options[option] ~= nil then
+            for _, glob in ipairs(conf.options[option]) do
+               table.insert(res[option], fs.normalize(fs.join(anchor_dir, glob)))
+            end
+         end
+      end
+
+      if conf.options.cache ~= nil then
+         if conf.options.cache == true then
+            if not res.cache then
+               res.cache = fs.normalize(fs.join(last_anchor_dir or current_dir, cache.get_default_dir()))
+            end
+         elseif conf.options.cache == false then
+            res.cache = false
+         else
+            res.cache = fs.normalize(fs.join(anchor_dir, conf.options.cache))
+         end
+      end
+   end
+
+   return res
+end
+
+local function add_applying_overrides(option_stack, conf, filename)
+   if not filename or not conf.options.files then
+      return
+   end
+
+   local current_dir = fs.get_current_dir()
+   local abs_filename = fs.normalize(fs.join(current_dir, filename))
+   local anchor_dir
+
+   if conf.anchor_dir == "" then
+      anchor_dir = fs.split_base(current_dir)
+   else
+      anchor_dir = conf.anchor_dir or current_dir
+   end
+
+   local matching_pairs = {}
+
+   for glob, opts in pairs(conf.options.files) do
+      if type(glob) == "string" then
+         local abs_glob = fs.normalize(fs.join(anchor_dir, glob))
+
+         if globbing.match(abs_glob, abs_filename) then
+            table.insert(matching_pairs, {
+               abs_glob = abs_glob,
+               opts = opts
+            })
+         end
+      end
+   end
+
+   table.sort(matching_pairs, function(pair1, pair2)
+      return globbing.compare(pair1.abs_glob, pair2.abs_glob)
+   end)
+
+   for _, pair in ipairs(matching_pairs) do
+      table.insert(option_stack, pair.opts)
+   end
+end
+
+-- Returns an option stack applicable to a file with given name, or in general if name is not given.
+function ConfigStack:get_options(filename)
+   local res = {}
+
+   for _, conf in ipairs(self._configs) do
+      table.insert(res, conf.options)
+      add_applying_overrides(res, conf, filename)
+   end
+
+   return res
+end
+
+return config
diff --git a/script/luacheck/core_utils.lua b/script/luacheck/core_utils.lua
new file mode 100755
index 000000000..a07892265
--- /dev/null
+++ b/script/luacheck/core_utils.lua
@@ -0,0 +1,95 @@
+local decoder = require "luacheck.decoder"
+local utils = require "luacheck.utils"
+
+local core_utils = {}
+
+-- Attempts to evaluate a node as a Lua value, without resolving locals.
+-- Returns Lua value and its string representation on success, nothing on failure.
+function core_utils.eval_const_node(node)
+   if node.tag == "True" then
+      return true, "true"
+   elseif node.tag == "False" then
+      return false, "false"
+   elseif node.tag == "String" then
+      local chars = decoder.decode(node[1])
+      return node[1], chars:get_printable_substring(1, chars:get_length())
+   else
+      local is_negative
+
+      if node.tag == "Op" and node[1] == "unm" then
+         is_negative = true
+         node = node[2]
+      end
+
+      if node.tag ~= "Number" then
+         return
+      end
+
+      local str = node[1]
+
+      if str:find("[iIuUlL]") then
+         -- Ignore LuaJIT cdata literals.
+         return
+      end
+
+      -- On Lua 5.3 convert to float to get same results as on Lua 5.1 and 5.2.
+      if _VERSION == "Lua 5.3" and not str:find("[%.eEpP]") then
+         str = str .. ".0"
+      end
+
+      local number = tonumber(str)
+
+      if not number then
+         return
+      end
+
+      if is_negative then
+         number = -number
+      end
+
+      if number == number and number < 1/0 and number > -1/0 then
+         return number, (is_negative and "-" or "") .. node[1]
+      end
+   end
+end
+
+local statement_containing_tags = utils.array_to_set({"Do", "While", "Repeat", "Fornum", "Forin", "If"})
+
+-- `items` is an array of nodes or nested item arrays.
+local function scan_for_statements(chstate, items, tags, callback, ...)
+   for _, item in ipairs(items) do
+      if tags[item.tag] then
+         callback(chstate, item, ...)
+      end
+
+      if not item.tag or statement_containing_tags[item.tag] then
+         scan_for_statements(chstate, item, tags, callback, ...)
+      end
+   end
+end
+
+-- Calls `callback(chstate, node, ...)` for each statement node within AST with tag in given array.
+function core_utils.each_statement(chstate, tags_array, callback, ...)
+   local tags = utils.array_to_set(tags_array)
+
+   for _, line in ipairs(chstate.lines) do
+      scan_for_statements(chstate, line.node[2], tags, callback, ...)
+   end
+end
+
+local function location_comparator(warning1, warning2)
+   if warning1.line ~= warning2.line then
+      return warning1.line < warning2.line
+   elseif warning1.column ~= warning2.column then
+      return warning1.column < warning2.column
+   else
+      return warning1.code < warning2.code
+   end
+end
+
+-- Sorts an array of warnings by location information as provided in `line` and `column` fields.
+function core_utils.sort_by_location(warnings)
+   table.sort(warnings, location_comparator)
+end
+
+return core_utils
diff --git a/script/luacheck/decoder.lua b/script/luacheck/decoder.lua
new file mode 100755
index 000000000..e2d36636c
--- /dev/null
+++ b/script/luacheck/decoder.lua
@@ -0,0 +1,199 @@
+local unicode = require "luacheck.unicode"
+local utils = require "luacheck.utils"
+
+local decoder = {}
+
+local sbyte = string.byte
+local sfind = string.find
+local sgsub = string.gsub
+local ssub = string.sub
+
+-- `LatinChars` and `UnicodeChars` objects represent source strings
+-- and provide Unicode-aware access to them with a common interface.
+-- Source bytes should not be accessed directly.
+-- Provided methods are:
+-- `Chars:get_codepoint(index)`: returns codepoint at given index as integer or nil if index is out of range.
+-- `Chars:get_substring(from, to)`: returns substring of original bytes corresponding to characters from `from` to `to`.
+-- `Chars:get_printable_substring(from. to)`: like get_substring but escapes not printable characters.
+-- `Chars:get_length()`: returns total number of characters.
+-- `Chars:find(pattern, from)`: `string.find` but `from` is in characters. Return values are still in bytes.
+
+-- `LatinChars` is an optimized special case for latin1 strings.
+local LatinChars = utils.class()
+
+function LatinChars:__init(bytes)
+   self._bytes = bytes
+end
+
+function LatinChars:get_codepoint(index)
+   return sbyte(self._bytes, index)
+end
+
+function LatinChars:get_substring(from, to)
+   return ssub(self._bytes, from, to)
+end
+
+local function hexadecimal_escaper(byte)
+   return ("\\x%02X"):format(sbyte(byte))
+end
+
+function LatinChars:get_printable_substring(from, to)
+   return (sgsub(ssub(self._bytes, from, to), "[^\32-\126]", hexadecimal_escaper))
+end
+
+function LatinChars:get_length()
+   return #self._bytes
+end
+
+function LatinChars:find(pattern, from)
+   return sfind(self._bytes, pattern, from)
+end
+
+-- Decodes `bytes` as UTF8. Returns arrays of codepoints as integers and their byte offsets.
+-- Byte offsets have one extra item pointing to one byte past the end of `bytes`.
+-- On decoding error returns nothing.
+local function get_codepoints_and_byte_offsets(bytes)
+   local codepoints = {}
+   local byte_offsets = {}
+
+   local byte_index = 1
+   local codepoint_index = 1
+
+   while true do
+      byte_offsets[codepoint_index] = byte_index
+
+      -- Attempt to decode the next codepoint from UTF8.
+      local codepoint = sbyte(bytes, byte_index)
+
+      if not codepoint then
+         return codepoints, byte_offsets
+      end
+
+      byte_index = byte_index + 1
+
+      if codepoint >= 0x80 then
+         -- Not ASCII.
+
+         if codepoint < 0xC0 then
+            return
+         end
+
+         local cont = (sbyte(bytes, byte_index) or 0) - 0x80
+
+         if cont < 0 or cont >= 0x40 then
+            return
+         end
+
+         byte_index = byte_index + 1
+
+         if codepoint < 0xE0 then
+            -- Two bytes.
+            codepoint = cont + (codepoint - 0xC0) * 0x40
+         elseif codepoint < 0xF0 then
+            -- Three bytes.
+            codepoint = cont + (codepoint - 0xE0) * 0x40
+
+            cont = (sbyte(bytes, byte_index) or 0) - 0x80
+
+            if cont < 0 or cont >= 0x40 then
+               return
+            end
+
+            byte_index = byte_index + 1
+
+            codepoint = cont + codepoint * 0x40
+         elseif codepoint < 0xF8 then
+            -- Four bytes.
+            codepoint = cont + (codepoint - 0xF0) * 0x40
+
+            cont = (sbyte(bytes, byte_index) or 0) - 0x80
+
+            if cont < 0 or cont >= 0x40 then
+               return
+            end
+
+            byte_index = byte_index + 1
+
+            codepoint = cont + codepoint * 0x40
+
+            cont = (sbyte(bytes, byte_index) or 0) - 0x80
+
+            if cont < 0 or cont >= 0x40 then
+               return
+            end
+
+            byte_index = byte_index + 1
+
+            codepoint = cont + codepoint * 0x40
+
+            if codepoint > 0x10FFFF then
+               return
+            end
+         else
+            return
+         end
+      end
+
+      codepoints[codepoint_index] = codepoint
+      codepoint_index = codepoint_index + 1
+   end
+end
+
+-- `UnicodeChars` is the general case for non-latin1 strings.
+-- Assumes UTF8, on decoding error falls back to latin1.
+local UnicodeChars = utils.class()
+
+function UnicodeChars:__init(bytes, codepoints, byte_offsets)
+   self._bytes = bytes
+   self._codepoints = codepoints
+   self._byte_offsets = byte_offsets
+end
+
+function UnicodeChars:get_codepoint(index)
+   return self._codepoints[index]
+end
+
+function UnicodeChars:get_substring(from, to)
+   local byte_offsets = self._byte_offsets
+   return ssub(self._bytes, byte_offsets[from], byte_offsets[to + 1] - 1)
+end
+
+function UnicodeChars:get_printable_substring(from, to)
+   -- This is only called on syntax error, it's okay to be slow.
+   local parts = {}
+
+   for index = from, to do
+      local codepoint = self._codepoints[index]
+
+      if unicode.is_printable(codepoint) then
+         table.insert(parts, self:get_substring(index, index))
+      else
+         table.insert(parts, (codepoint > 255 and "\\u{%X}" or "\\x%02X"):format(codepoint))
+      end
+   end
+
+   return table.concat(parts)
+end
+
+function UnicodeChars:get_length()
+   return #self._codepoints
+end
+
+function UnicodeChars:find(pattern, from)
+   return sfind(self._bytes, pattern, self._byte_offsets[from])
+end
+
+function decoder.decode(bytes)
+   -- Only use UnicodeChars if necessary. LatinChars isn't much faster but noticeably more memory efficient.
+   if sfind(bytes, "[\128-\255]") then
+      local codepoints, byte_offsets = get_codepoints_and_byte_offsets(bytes)
+
+      if codepoints then
+         return UnicodeChars(bytes, codepoints, byte_offsets)
+      end
+   end
+
+   return LatinChars(bytes)
+end
+
+return decoder
diff --git a/script/luacheck/expand_rockspec.lua b/script/luacheck/expand_rockspec.lua
new file mode 100755
index 000000000..7737daa43
--- /dev/null
+++ b/script/luacheck/expand_rockspec.lua
@@ -0,0 +1,110 @@
+local fs = require "luacheck.fs"
+local utils = require "luacheck.utils"
+
+local blacklist = utils.array_to_set({"spec", ".luarocks", "lua_modules", "test.lua", "tests.lua"})
+
+-- This reimplements relevant parts of `luarocks.build.builtin.autodetect_modules`.
+-- Autodetection works relatively to the directory containing the rockspec.
+local function autodetect_modules(rockspec_path)
+   rockspec_path = fs.normalize(rockspec_path)
+   local base, rest = fs.split_base(rockspec_path)
+   local project_dir = base .. (rest:match("^(.*)" .. utils.dir_sep .. ".*$") or "")
+
+   if project_dir == "" then
+      project_dir = "."
+   end
+
+   local module_dir = project_dir
+
+   for _, module_subdir in ipairs({"src", "lua", "lib"}) do
+      local full_module_dir = fs.join(project_dir, module_subdir)
+
+      if fs.is_dir(full_module_dir) then
+         module_dir = full_module_dir
+         break
+      end
+   end
+
+   local res = {}
+
+   for _, file in ipairs((fs.extract_files(module_dir, "%.lua$"))) do
+      -- Extract first part of the path from module_dir to the file, or file name itself.
+      if not blacklist[file:match("^" .. module_dir:gsub("%p", "%%%0") .. "[\\/]*([^\\/]*)")] then
+         table.insert(res, file)
+      end
+   end
+
+   local bin_dir
+
+   for _, bin_subdir in ipairs({"src/bin", "bin"}) do
+      local full_bin_dir = fs.join(project_dir, bin_subdir)
+
+      if fs.is_dir(full_bin_dir) then
+         bin_dir = full_bin_dir
+      end
+   end
+
+   if bin_dir then
+      local iter, state, var = fs.dir_iter(bin_dir)
+
+      if iter then
+         for basename in iter, state, var do
+            if basename:sub(-#".lua") == ".lua" then
+               table.insert(res, fs.join(bin_dir, basename))
+            end
+         end
+      end
+   end
+
+   return res
+end
+
+local function extract_lua_files(rockspec_path, rockspec)
+   local build = rockspec.build
+
+   if type(build) ~= "table" then
+      return autodetect_modules(rockspec_path)
+   end
+
+   if not build.type or build.type == "builtin" or build.type == "module" then
+      if not build.modules then
+         return autodetect_modules(rockspec_path)
+      end
+   end
+
+   local res = {}
+
+   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
+
+   scan(build.modules)
+
+   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.
+-- On error returns nil and "I/O", "syntax", or "runtime" and error message.
+local function expand_rockspec(rockspec_path)
+   local rockspec, err_type, err_msg = utils.load_config(rockspec_path)
+
+   if not rockspec then
+      return nil, err_type, err_msg
+   end
+
+   return extract_lua_files(rockspec_path, rockspec)
+end
+
+return expand_rockspec
diff --git a/script/luacheck/filter.lua b/script/luacheck/filter.lua
new file mode 100755
index 000000000..5ef8fd1bc
--- /dev/null
+++ b/script/luacheck/filter.lua
@@ -0,0 +1,535 @@
+local core_utils = require "luacheck.core_utils"
+local decoder = require "luacheck.decoder"
+local options = require "luacheck.options"
+local utils = require "luacheck.utils"
+
+local filter = {}
+
+-- Returns two optional booleans indicating if warning matches pattern by code and name.
+local function match(pattern, code, name)
+   local matches_code, matches_name
+   local code_pattern, name_pattern = pattern[1], pattern[2]
+
+   if code_pattern then
+      matches_code = utils.pmatch(code, code_pattern)
+   end
+
+   if name_pattern then
+      if not name then
+         -- Warnings without name field can't match by name.
+         matches_name = false
+      else
+         matches_name = utils.pmatch(name, name_pattern)
+      end
+   end
+
+   return matches_code, matches_name
+end
+
+local function passes_rules_filter(rules, code, name)
+   -- A warning is enabled when its code and name are enabled.
+   local enabled_code, enabled_name = false, false
+
+   for _, rule in ipairs(rules) do
+      local matches_one = false
+
+      for _, pattern in ipairs(rule[1]) do
+         local matches_code, matches_name = match(pattern, code, name)
+
+         -- If a factor is enabled, warning can't be disabled by it.
+         if enabled_code then
+            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 = {}
+
+   if warning.indexing then
+      for _, index in ipairs(warning.indexing) do
+         local part
+
+         if type(index) == "string" then
+            local chars = decoder.decode(index)
+            part = chars:get_printable_substring(1, chars:get_length())
+         else
+            part = "?"
+         end
+
+         table.insert(parts, part)
+      end
+   end
+
+   return table.concat(parts, ".")
+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 and #warning.indexing or 0) + 1 do
+      local index_string = i == 1 and warning.name or warning.indexing[i - 1]
+
+      if index_string == true then
+         -- Indexing with something that may or may not be a string.
+         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
+
+-- Checks if a warning passes options filter. May add some fields required for formatting.
+local function passes_filter(normalized_options, warning)
+   if warning.code == "561" then
+      local max_complexity = normalized_options.max_cyclomatic_complexity
+
+      if not max_complexity or warning.complexity <= max_complexity then
+         return false
+      end
+
+      warning.max_complexity = max_complexity
+   elseif warning.code:find("^[234]") and warning.name == "_" and not warning.useless then
+      return false
+   elseif warning.code:find("^1[14]") then
+      if warning.indirect and
+            get_field_status(normalized_options, warning, warning.previous_indexing_len) == "undefined" then
+         return false
+      end
+
+      if not warning.module and get_field_status(normalized_options, warning) ~= "undefined" then
+         return false
+      end
+   end
+
+   if warning.code:find("^1[24][23]") then
+      warning.field = get_field_string(warning)
+   end
+
+   if warning.secondary and not normalized_options.unused_secondaries then
+      return false
+   end
+
+   if warning.self and not normalized_options.self then
+      return false
+   end
+
+   return passes_rules_filter(normalized_options.rules, warning.code, warning.name)
+end
+
+local empty_options = {}
+
+-- Updates option_stack for given line with next_index pointing to the inline option past the previous line.
+-- Adds warnings for invalid inline options to check_result, filtered_warnings.
+-- Returns updated next_index.
+local function update_option_stack_for_new_line(check_result, stds, option_stack, line, next_index)
+   local inline_option = check_result.inline_options[next_index]
+
+   if not inline_option or inline_option.line > line then
+      -- No inline options on this line, option stack for the line is ready.
+      return next_index
+   end
+
+   next_index = next_index + 1
+
+   if inline_option.pop_count then
+      for _ = 1, inline_option.pop_count do
+         table.remove(option_stack)
+      end
+   end
+
+   if not inline_option.options then
+      -- No inline option push on this line, option stack for the line is ready.
+      return next_index
+   end
+
+   local options_ok, err_msg = options.validate(options.all_options, inline_option.options, stds)
+
+   if not options_ok then
+      -- Warn about invalid inline option, push a dummy empty table instead to keep pop counts correct.
+      inline_option.options = nil
+      inline_option.code = "021"
+      inline_option.msg = err_msg
+      table.insert(check_result.filtered_warnings, inline_option)
+
+      -- Reuse empty table identity so that normalized option caching works better.
+      table.insert(option_stack, empty_options)
+   else
+      table.insert(option_stack, inline_option.options)
+   end
+
+   return next_index
+end
+
+-- Warns (adds to check_result.filtered_warnings) about a line if it's too long
+-- and the warning is not filtered out by options.
+local function check_line_length(check_result, normalized_options, line)
+   local line_length = check_result.line_lengths[line]
+   local line_type = check_result.line_endings[line]
+   local max_length = normalized_options["max_" .. (line_type or "code") .. "_line_length"]
+
+   if max_length and line_length > max_length then
+      if passes_rules_filter(normalized_options.rules, "631") then
+         table.insert(check_result.filtered_warnings, {
+            code = "631",
+            line = line,
+            column = max_length + 1,
+            end_column = line_length,
+            max_length = max_length,
+            line_ending = line_type
+         })
+      end
+   end
+end
+
+-- Adds warnings passing filtering and not related to globals to check_result.filtered_warnings.
+-- If there is a global related warning on this line, sets check_results[line] to normalized_optuons.
+local function filter_warnings_on_new_line(check_result, normalized_options, line, next_index)
+   while true do
+      local warning = check_result.warnings[next_index]
+
+      if not warning or warning.line > line then
+         -- No more warnings on this line.
+         break
+      end
+
+      if warning.code:find("^1") then
+         check_result.normalized_options[line] = normalized_options
+      elseif passes_filter(normalized_options, warning) then
+         table.insert(check_result.filtered_warnings, warning)
+      end
+
+      next_index = next_index + 1
+   end
+
+   return next_index
+end
+
+-- Normalizing options is relatively expensive because full std definitions are quite large.
+-- `CachingOptionsNormalizer` implements a caching layer that reduces number of `options.normalize` calls.
+-- Caching is done based on identities of option tables.
+
+local CachingOptionsNormalizer = utils.class()
+
+function CachingOptionsNormalizer:__init()
+   self.result_trie = {}
+end
+
+function CachingOptionsNormalizer:normalize_options(stds, option_stack)
+   local result_node = self.result_trie
+
+   for _, option_table in ipairs(option_stack) do
+      if not result_node[option_table] then
+         result_node[option_table] = {}
+      end
+
+      result_node = result_node[option_table]
+   end
+
+   if result_node.result then
+      return result_node.result
+   end
+
+   local result = options.normalize(option_stack, stds)
+   result_node.result = result
+   return result
+end
+
+-- May mutate base_opts_stack.
+local function filter_not_global_related_in_file(check_result, options_normalizer, stds, option_stack)
+   check_result.filtered_warnings = {}
+   check_result.normalized_options = {}
+
+   -- Iterate over lines, warnings, and inline options at the same time, keeping opts_stack up to date.
+   local next_warning_index = 1
+   local next_inline_option_index = 1
+
+   for line in ipairs(check_result.line_lengths) do
+      next_inline_option_index = update_option_stack_for_new_line(
+         check_result, stds, option_stack, line, next_inline_option_index)
+      local normalized_options = options_normalizer:normalize_options(stds, option_stack)
+      check_line_length(check_result, normalized_options, line)
+      next_warning_index = filter_warnings_on_new_line(check_result, normalized_options, line, next_warning_index)
+   end
+end
+
+local function may_have_options(opts_table)
+   for key in pairs(opts_table) do
+      if type(key) == "string" then
+         return true
+      end
+   end
+
+   return false
+end
+
+local function get_option_stack(opts, file_index)
+   local res = {opts}
+
+   if opts and opts[file_index] then
+      -- Don't add useless per-file option tables, that messes up normalized option caching
+      -- since it memorizes based on option table identities.
+      if may_have_options(opts[file_index]) then
+         table.insert(res, opts[file_index])
+      end
+
+      for _, nested_opts in ipairs(opts[file_index]) do
+         table.insert(res, nested_opts)
+      end
+   end
+
+   return res
+end
+
+-- For each file check result:
+-- * Stores invalid inline options, not filtered out not global-related warnings, and newly created line length warnings
+--   in .filtered_warnings.
+-- * Stores a map from line numbers to normalized options for lines of global-related warnings in .normalized_options.
+local function filter_not_global_related(check_results, opts, stds)
+   local caching_options_normalizer = CachingOptionsNormalizer()
+
+   for file_index, check_result in ipairs(check_results) do
+      if not check_result.fatal then
+         if check_result.warnings[1] and check_result.warnings[1].code == "011" then
+            -- Special case syntax errors, they don't have line numbers so normal filtering does not work.
+            check_result.filtered_warnings = check_result.warnings
+            check_result.normalized_options = {}
+         else
+            local base_file_option_stack = get_option_stack(opts, file_index)
+            filter_not_global_related_in_file(check_result, caching_options_normalizer, stds, base_file_option_stack)
+         end
+      end
+   end
+end
+
+-- 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.
+
+local function is_definition(normalized_options, warning)
+   return normalized_options.allow_defined or (normalized_options.allow_defined_top and warning.top)
+end
+
+-- Extracts sets of defined, exported and used globals from a file check result.
+local function get_implicit_globals_in_file(check_result)
+   local defined = {}
+   local exported = {}
+   local used = {}
+
+   for _, warning in ipairs(check_result.warnings) do
+      if warning.code:find("^11") then
+         if warning.code == "111" then
+            local normalized_options = check_result.normalized_options[warning.line]
+
+            if is_definition(normalized_options, warning) then
+               if normalized_options.module then
+                  defined[warning.name] = true
+               else
+                  exported[warning.name] = true
+               end
+            end
+         else
+            used[warning.name] = true
+         end
+      end
+   end
+
+   return defined, exported, used
+end
+
+-- Returns set of globals defines across all files except modules, a set of globals used across all files,
+-- and an array of sets of globals defined per file, parallel to the check results array.
+local function get_implicit_globals(check_results)
+   local globally_defined = {}
+   local globally_used = {}
+   local locally_defined = {}
+
+   for file_index, check_result in ipairs(check_results) do
+      if not check_result.fatal then
+         local defined, exported, used = get_implicit_globals_in_file(check_result)
+         utils.update(globally_defined, exported)
+         utils.update(globally_used, used)
+         locally_defined[file_index] = defined
+      end
+   end
+
+   return globally_defined, globally_used, locally_defined
+end
+
+-- Mutates the warning and returns it or discards it by returning nothing if it's filtered out.
+local function apply_implicit_definitions(globally_defined, globally_used, locally_defined, normalized_options, warning)
+   if not warning.code:find("^11") then
+      return warning
+   end
+
+   if warning.code == "111" then
+      if normalized_options.module then
+         if locally_defined[warning.name] then
+            return
+         end
+
+         warning.module = true
+      else
+         if is_definition(normalized_options, warning) then
+            if globally_used[warning.name] then
+               return
+            end
+
+            warning.code = "131"
+            warning.top = nil
+         else
+            if globally_defined[warning.name] then
+               return
+            end
+         end
+      end
+   else
+      if globally_defined[warning.name] or locally_defined[warning.name] then
+         return
+      end
+   end
+
+   return warning
+end
+
+local function filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined)
+   for _, warning in ipairs(check_result.warnings) do
+      if warning.code:find("^1") then
+         local normalized_options = check_result.normalized_options[warning.line]
+         warning = apply_implicit_definitions(
+            globally_defined, globally_used, locally_defined, normalized_options, warning)
+
+         if warning then
+            if warning.code:find("^11[12]") and not warning.module and
+                  get_field_status(normalized_options, warning) == "read_only" then
+               warning.code = "12" .. warning.code:sub(3, 3)
+            elseif warning.code:find("^11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then
+               warning.code = "14" .. warning.code:sub(3, 3)
+            end
+
+            if warning.code:match("11[23]") and get_field_status(normalized_options, warning, 1) ~= "undefined" then
+               warning.code = "14" .. warning.code:sub(3, 3)
+            end
+
+            if passes_filter(normalized_options, warning) then
+               table.insert(check_result.filtered_warnings, warning)
+            end
+         end
+      end
+   end
+end
+
+local function filter_global_related(check_results)
+   local globally_defined, globally_used, locally_defined = get_implicit_globals(check_results)
+
+   for file_index, check_result in ipairs(check_results) do
+      if not check_result.fatal then
+         filter_global_related_in_file(check_result, globally_defined, globally_used, locally_defined[file_index])
+      end
+   end
+end
+
+-- Processes an array of results of the check stage (or tables with .fatal field) into the final report.
+-- `opts[i]`, if present, is used as options when processing `report[i]` together with options in its array part.
+-- This function may mutate check results or reuse its parts in the return value.
+function filter.filter(check_results, opts, stds)
+   filter_not_global_related(check_results, opts, stds)
+   filter_global_related(check_results)
+
+   local report = {}
+
+   for file_index, check_result in ipairs(check_results) do
+      if check_result.fatal then
+         report[file_index] = check_result
+      else
+         core_utils.sort_by_location(check_result.filtered_warnings)
+         report[file_index] = check_result.filtered_warnings
+      end
+   end
+
+   return report
+end
+
+return filter
diff --git a/script/luacheck/format.lua b/script/luacheck/format.lua
new file mode 100755
index 000000000..96f39a5fd
--- /dev/null
+++ b/script/luacheck/format.lua
@@ -0,0 +1,334 @@
+local stages = require "luacheck.stages"
+local utils = require "luacheck.utils"
+
+local format = {}
+
+local color_support = not utils.is_windows or os.getenv("ANSICON")
+
+local function get_message_format(warning)
+   local message_format = assert(stages.warnings[warning.code], "Unkown warning code " .. warning.code).message_format
+
+   if type(message_format) == "function" then
+      return message_format(warning)
+   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)
+      table.insert(buf, "")
+   end
+
+   return table.concat(buf, "\n")
+end
+
+local function escape_xml(str)
+   str = str:gsub("&", "&amp;")
+   str = str:gsub('"', "&quot;")
+   str = str:gsub("'", "&apos;")
+   str = str:gsub("<", "&lt;")
+   str = str:gsub(">", "&gt;")
+   return str
+end
+
+format.builtin_formatters = {}
+
+function format.builtin_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 format.builtin_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 format.builtin_formatters.JUnit(report, file_names)
+   -- JUnit formatter doesn't support any options.
+   local opts = {}
+   local buf = {[[<?xml version="1.0" encoding="UTF-8"?>]]}
+   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, ([[<testsuite name="Luacheck report" tests="%d">]]):format(num_testcases))
+
+   for file_i, file_report in ipairs(report) do
+      if file_report.fatal then
+         table.insert(buf, ([[    <testcase name="%s" classname="%s">]]):format(
+            escape_xml(file_names[file_i]), escape_xml(file_names[file_i])))
+         table.insert(buf, ([[        <error type="%s"/>]]):format(escape_xml(fatal_type(file_report))))
+         table.insert(buf, [[    </testcase>]])
+      elseif #file_report == 0 then
+         table.insert(buf, ([[    <testcase name="%s" classname="%s"/>]]):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, ([[    <testcase name="%s:%d" classname="%s">]]):format(
+               escape_xml(file_names[file_i]), event_i, escape_xml(file_names[file_i])))
+            table.insert(buf, ([[        <failure type="%s" message="%s"/>]]):format(
+               escape_xml(event_code(event)), escape_xml(format_event(file_names[file_i], event, opts))))
+            table.insert(buf, [[    </testcase>]])
+         end
+      end
+   end
+
+   table.insert(buf, [[</testsuite>]])
+   return table.concat(buf, "\n")
+end
+
+local fatal_error_codes = {
+   ["I/O"] = "F1",
+   ["syntax"] = "F2",
+   ["runtime"] = "F3"
+}
+
+function format.builtin_formatters.visual_studio(report, file_names)
+   local buf = {}
+
+   for i, file_report in ipairs(report) do
+      if file_report.fatal then
+         -- Older docs suggest that line number after a file name is optional; newer docs mark it as required.
+         -- Just use tool name as origin and put file name into the message.
+         table.insert(buf, ("luacheck : fatal error %s: couldn't check %s: %s"):format(
+            fatal_error_codes[file_report.fatal], file_names[i], file_report.msg))
+      else
+         for _, event in ipairs(file_report) do
+               -- Older documentation on the format suggests that it could support column range.
+               -- Newer docs don't mention it. Don't use it for now.
+               local event_type = event.code:sub(1, 1) == "0" and "error" or "warning"
+               local message = format_message(event)
+               table.insert(buf, ("%s(%d,%d) : %s %s: %s"):format(
+                  file_names[i], event.line, event.column, event_type, event_code(event), message))
+         end
+      end
+   end
+
+   return table.concat(buf, "\n")
+end
+
+function format.builtin_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 format.builtin_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
diff --git a/script/luacheck/fs.lua b/script/luacheck/fs.lua
new file mode 100755
index 000000000..101e6a1f9
--- /dev/null
+++ b/script/luacheck/fs.lua
@@ -0,0 +1,219 @@
+local fs = {}
+
+local lfs = require "lfs"
+local utils = require "luacheck.utils"
+
+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
+
+function fs.is_absolute(path)
+   return fs.split_base(path) ~= ""
+end
+
+function fs.normalize(path)
+   if utils.is_windows then
+      path = path:lower()
+   end
+   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 fs.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 lfs.attributes(path, "mode") == "directory"
+end
+
+function fs.is_file(path)
+   return lfs.attributes(path, "mode") == "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 fs.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
+         return
+      end
+
+      rest = rest:match("^(.*)"..utils.dir_sep..".*$") or ""
+      rel_path = rel_path..".."..utils.dir_sep
+   end
+end
+
+-- Returns iterator over directory items or nil, error message.
+function fs.dir_iter(dir_path)
+   local ok, iter, state, var = pcall(lfs.dir, dir_path)
+
+   if not ok then
+      local err = utils.unprefix(iter, "cannot open " .. dir_path .. ": ")
+      return nil, "couldn't list directory: " .. err
+   end
+
+   return iter, state, var
+end
+
+-- Returns list of all files in directory matching pattern.
+-- Additionally returns a mapping from directory paths that couldn't be expanded
+-- to error messages.
+function fs.extract_files(dir_path, pattern)
+   local res = {}
+   local err_map = {}
+
+   local function scan(dir)
+      local iter, state, var = fs.dir_iter(dir)
+
+      if not iter then
+         err_map[dir] = state
+         table.insert(res, dir)
+         return
+      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
+               scan(full_path)
+            elseif path:match(pattern) and fs.is_file(full_path) then
+               table.insert(res, full_path)
+            end
+         end
+      end
+   end
+
+   scan(dir_path)
+   table.sort(res)
+   return res, err_map
+end
+
+local function make_absolute_dirs(dir_path)
+   if fs.is_dir(dir_path) then
+      return true
+   end
+
+   local upper_dir = fs.normalize(fs.join(dir_path, ".."))
+
+   if upper_dir == dir_path then
+      return nil, ("Filesystem root %s is not a directory"):format(upper_dir)
+   end
+
+   local upper_ok, upper_err = make_absolute_dirs(upper_dir)
+
+   if not upper_ok then
+      return nil, upper_err
+   end
+
+   local make_ok, make_error = lfs.mkdir(dir_path)
+
+   if not make_ok then
+      return nil, ("Couldn't make directory %s: %s"):format(dir_path, make_error)
+   end
+
+   return true
+end
+
+-- Ensures that a given path is a directory, creating intermediate directories if necessary.
+-- Returns true on success, nil and an error message on failure.
+function fs.make_dirs(dir_path)
+   return make_absolute_dirs(fs.normalize(fs.join(fs.get_current_dir(), dir_path)))
+end
+
+-- Returns modification time for a file.
+function fs.get_mtime(path)
+   return lfs.attributes(path, "modification")
+end
+
+-- Returns absolute path to current working directory, with trailing directory separator.
+function fs.get_current_dir()
+   return ensure_dir_sep(assert(lfs.currentdir()))
+end
+
+return fs
diff --git a/script/luacheck/globbing.lua b/script/luacheck/globbing.lua
new file mode 100755
index 000000000..b5337e182
--- /dev/null
+++ b/script/luacheck/globbing.lua
@@ -0,0 +1,161 @@
+local 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 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.
+-- Both must be absolute.
+function globbing.match(glob, 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
diff --git a/script/luacheck/init.lua b/script/luacheck/init.lua
new file mode 100755
index 000000000..7fe7aed0c
--- /dev/null
+++ b/script/luacheck/init.lua
@@ -0,0 +1,135 @@
+local 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.23.0"
+}
+
+local function raw_validate_options(fname, opts, stds, context)
+   local ok, err = options.validate(options.all_options, opts, stds)
+
+   if not ok then
+      if context then
+         error(("bad argument #2 to '%s' (%s: %s)"):format(fname, context, err))
+      else
+         error(("bad argument #2 to '%s' (%s)"):format(fname, err))
+      end
+   end
+end
+
+local function validate_options(fname, items, opts, stds)
+   raw_validate_options(fname, opts)
+
+   if opts ~= nil then
+      for i in ipairs(items) do
+         raw_validate_options(fname, opts[i], stds, ("invalid options at index [%d]"):format(i))
+
+         if opts[i] ~= nil then
+            for j, nested_opts in ipairs(opts[i]) do
+               raw_validate_options(fname, nested_opts, stds, ("invalid options at index [%d][%d]"):format(i, j))
+            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, stds)
+   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, stds)
+   local report = filter.filter(reports, opts, stds)
+   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
diff --git a/script/luacheck/lexer.lua b/script/luacheck/lexer.lua
new file mode 100755
index 000000000..e626ea54e
--- /dev/null
+++ b/script/luacheck/lexer.lua
@@ -0,0 +1,749 @@
+local 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 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)
+   local offset = state.offset + 1
+   state.offset = offset
+   return state.src:get_codepoint(offset)
+end
+
+-- Skipping helpers.
+-- Take the current character, skip something, return next character.
+
+local function skip_newline(state, newline)
+   local first_newline_offset = state.offset
+   local b = next_byte(state)
+
+   if b ~= newline and is_newline(b) then
+      b = next_byte(state)
+   end
+
+   local line = state.line
+   local line_offsets = state.line_offsets
+   state.line_lengths[line] = first_newline_offset - line_offsets[line]
+   line = line + 1
+   state.line = line
+   line_offsets[line] = state.offset
+   return b
+end
+
+local function skip_to_newline(state, b)
+   while not is_newline(b) and b 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] = state.src:get_substring(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] = state.src:get_substring(line_start, state.offset-opening_long_bracket-2)
+   state.offset = state.offset + 1
+   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] = state.src:get_substring(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] = state.src:get_substring(chunk_start, state.offset-1)
+      end
+
+      string_value = tconcat(chunks)
+   else
+      -- There were no escape sequences.
+      string_value = state.src:get_substring(chunk_start, state.offset-1)
+   end
+
+   -- Skip the closing quote.
+   state.offset = state.offset + 1
+   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".
+      state.offset = state.offset + 1
+   else
+      -- uint64_t and int64_t literals can not be fractional.
+      if not is_float then
+         if b == BYTE_u or b == BYTE_U then
+            -- It may be uint64_t literal.
+            local b1 = state.src:get_codepoint(state.offset+1)
+
+            if b1 == BYTE_l or b1 == BYTE_L then
+               local b2 = state.src:get_codepoint(state.offset+2)
+
+               if b2 == BYTE_l or b2 == BYTE_L then
+                  -- It is uint64_t literal.
+                  state.offset = state.offset + 3
+               end
+            end
+         elseif b == BYTE_l or b == BYTE_L then
+            -- It may be uint64_t or int64_t literal.
+            local b1 = state.src:get_codepoint(state.offset+1)
+
+            if b1 == BYTE_l or b1 == BYTE_L then
+               local b2 = state.src:get_codepoint(state.offset+2)
+
+               if b2 == BYTE_u or b2 == BYTE_U then
+                  -- It is uint64_t literal.
+                  state.offset = state.offset + 3
+               else
+                  -- It is int64_t literal.
+                  state.offset = state.offset + 2
+               end
+            end
+         end
+      end
+   end
+
+   return "number", state.src:get_substring(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 = state.src:get_substring(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 "-"
+   end
+
+   -- 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, "long_comment")
+      end
+   end
+
+   -- Short comment.
+   skip_to_newline(state, b)
+   local comment_value = state.src:get_substring(start, state.offset - 1)
+   return "short_comment", comment_value
+end
+
+local function lex_bracket(state)
+   -- 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
+      state.offset = state.offset + 1
+      return "=="
+   else
+      return "="
+   end
+end
+
+local function lex_lt(state)
+   local b = next_byte(state)
+
+   if b == BYTE_EQ then
+      state.offset = state.offset + 1
+      return "<="
+   elseif b == BYTE_LT then
+      state.offset = state.offset + 1
+      return "<<"
+   else
+      return "<"
+   end
+end
+
+local function lex_gt(state)
+   local b = next_byte(state)
+
+   if b == BYTE_EQ then
+      state.offset = state.offset + 1
+      return ">="
+   elseif b == BYTE_GT then
+      state.offset = state.offset + 1
+      return ">>"
+   else
+      return ">"
+   end
+end
+
+local function lex_div(state)
+   local b = next_byte(state)
+
+   if b == BYTE_SLASH then
+      state.offset = state.offset + 1
+      return "//"
+   else
+      return "/"
+   end
+end
+
+local function lex_ne(state)
+   local b = next_byte(state)
+
+   if b == BYTE_EQ then
+      state.offset = state.offset + 1
+      return "~="
+   else
+      return "~"
+   end
+end
+
+local function lex_colon(state)
+   local b = next_byte(state)
+
+   if b == BYTE_COLON then
+      state.offset = state.offset + 1
+      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
+         state.offset = state.offset + 1
+         return "...", "..."
+      else
+         return ".."
+      end
+   elseif b and to_dec(b) then
+      -- Backtrack to dot.
+      state.offset = state.offset - 2
+      return lex_number(state, next_byte(state))
+   else
+      return "."
+   end
+end
+
+local function lex_any(state, b)
+   state.offset = state.offset + 1
+
+   if b > 255 then
+      b = 255
+   end
+
+   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
+
+-- Creates and returns lexer state for source.
+function lexer.new_state(src, line_offsets, line_lengths)
+   local state = {
+      src = src,
+      line = 1,
+      line_offsets = line_offsets or {},
+      line_lengths = line_lengths or {},
+      offset = 1
+   }
+
+   state.line_offsets[1] = 1
+
+   if src:get_length() >= 2 and src:get_substring(1, 2) == "#!" then
+      -- Skip shebang line.
+      state.offset = 2
+      skip_to_newline(state, next_byte(state))
+   end
+
+   return state
+end
+
+function lexer.get_quoted_substring_or_line(state, line, offset, end_offset)
+   local line_length = state.line_lengths[line]
+
+   if line_length then
+      local line_end_offset = state.line_offsets[line] + line_length - 1
+
+      if line_end_offset < end_offset then
+         end_offset = line_end_offset
+      end
+   end
+
+   return "'" .. state.src:get_printable_substring(offset, end_offset) .. "'"
+end
+
+-- Looks for next token starting from state.line, state.offset.
+-- Returns next token, its value and its location (line, offset).
+-- Sets state.line, state.offset to token end location + 1.
+-- Fills state.line_offsets and state.line_lengths.
+-- On error returns nil, error message, error location (line, offset), error end offset.
+function lexer.next_token(state)
+   local line_offsets = state.line_offsets
+   local b = skip_space(state, state.src:get_codepoint(state.offset))
+
+   -- Save location of token start.
+   local token_line = state.line
+   local line_offset = line_offsets[token_line]
+   local token_offset = state.offset
+
+   if not b then
+      -- EOF token has length 1.
+      state.offset = state.offset + 1
+      state.line_lengths[token_line] = token_offset - line_offset
+      return "eof", nil, token_line, token_offset
+   end
+
+   local token, token_value, relative_error_offset = (byte_handlers[b] or lex_any)(state, b)
+
+   if relative_error_offset then
+      -- Error relative to current offset.
+      local error_offset = state.offset + relative_error_offset
+      local error_end_offset = math.min(state.offset, state.src:get_length())
+      local error_message = token_value .. " " .. lexer.get_quoted_substring_or_line(state,
+         state.line, error_offset, error_end_offset)
+      return nil, error_message, state.line, error_offset, error_end_offset
+   end
+
+   -- Single character errors fall through here.
+   return token, token_value, token_line, token_offset, not token and token_offset
+end
+
+return lexer
diff --git a/script/luacheck/main.lua b/script/luacheck/main.lua
new file mode 100755
index 000000000..8366ee154
--- /dev/null
+++ b/script/luacheck/main.lua
@@ -0,0 +1,353 @@
+local argparse = require "argparse"
+local cache = require "luacheck.cache"
+local config = require "luacheck.config"
+local luacheck = require "luacheck"
+local multithreading = require "luacheck.multithreading"
+local profiler = require "luacheck.profiler"
+local runner = require "luacheck.runner"
+local utils = require "luacheck.utils"
+local version = require "luacheck.version"
+
+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 get_parser()
+   local parser = argparse(
+      "luacheck", "luacheck " .. luacheck._VERSION .. ", a linter and a static analyzer for Lua.", [[
+Links:
+
+   Luacheck on GitHub: https://github.com/mpeterv/luacheck
+   Luacheck documentation: https://luacheck.readthedocs.org]])
+      :help_max_width(80)
+
+   parser:argument("files", "List of files, directories and rockspecs to check. Pass '-' to check stdin.")
+      :args "+"
+      :argname "<file>"
+
+   parser:group("Options for filtering warnings",
+      parser:flag("-g --no-global", "Filter out warnings related to global variables. " ..
+         "Equivalent to --ignore 1."):target("global"):action("store_false"),
+      parser:flag("-u --no-unused", "Filter out warnings related to unused variables and values. " ..
+         "Equivalent to --ignore [23]."):target("unused"):action("store_false"),
+      parser:flag("-r --no-redefined", "Filter out warnings related to redefined variables. " ..
+         "Equivalent to --ignore 4."):target("redefined"):action("store_false"),
+
+      parser:flag("-a --no-unused-args", "Filter out warnings related to unused arguments and " ..
+         "loop variables. Equivalent to --ignore 21[23]."):target("unused_args"):action("store_false"),
+      parser:flag("-s --no-unused-secondaries", "Filter out warnings related to unused variables set " ..
+         "together with used ones."):target("unused_secondaries"):action("store_false"),
+      parser:flag("--no-self", "Filter out warnings related to implicit self argument.")
+         :target("self"):action("store_false"),
+
+      parser:option("--ignore -i", "Filter out warnings matching these patterns.\n" ..
+         "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 "<patt>"
+         :action "concat"
+         :init(nil),
+      parser:option("--enable -e", "Do not filter out warnings matching these patterns.")
+         :args "+"
+         :count "*"
+         :argname "<patt>"
+         :action "concat"
+         :init(nil),
+      parser:option("--only -o", "Filter out warnings not matching these patterns.")
+         :args "+"
+         :count "*"
+         :argname "<patt>"
+         :action "concat"
+         :init(nil))
+
+   parser:group("Options for configuring allowed globals",
+      parser:option("--std", "Set standard globals, default is max. <std> can be one of:\n" ..
+         "   max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" ..
+         "   min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" ..
+         "   lua51 - globals of Lua 5.1 without deprecated ones;\n" ..
+         "   lua51c - globals of Lua 5.1;\n" ..
+         "   lua52 - globals of Lua 5.2;\n" ..
+         "   lua52c - globals of Lua 5.2 with LUA_COMPAT_ALL;\n" ..
+         "   lua53 - globals of Lua 5.3;\n" ..
+         "   lua53c - globals of Lua 5.3 with LUA_COMPAT_5_2;\n" ..
+         "   luajit - globals of LuaJIT 2.x;\n" ..
+         "   ngx_lua - globals of Openresty lua-nginx-module 0.10.10, including standard LuaJIT 2.x globals;\n" ..
+         "   love - globals added by LÖVE;\n" ..
+         "   busted - globals added by Busted 2.0, by default added for files ending with _spec.lua within spec, " ..
+         "test, and tests subdirectories;\n" ..
+         "   rockspec - globals allowed in rockspecs, by default added for files ending with .rockspec;\n" ..
+         "   luacheckrc - globals allowed in Luacheck configs, by default added for files ending with .luacheckrc;\n" ..
+         "   none - no standard globals.\n\n" ..
+         "Sets can be combined using '+'. Extra sets can be defined in config by " ..
+         "adding to `stds` global in config."),
+      parser:flag("-c --compat", "Equivalent to --std max."),
+
+      parser:option("--globals", "Add custom global variables (e.g. foo) or fields (e.g. foo.bar) " ..
+         "on top of standard ones.")
+         :args "*"
+         :count "*"
+         :argname "<name>"
+         :action "concat"
+         :init(nil),
+      parser:option("--read-globals", "Add read-only global variables or fields.")
+         :args "*"
+         :count "*"
+         :argname "<name>"
+         :action "concat"
+         :init(nil),
+      parser:option("--new-globals", "Set custom global variables or fields. Removes custom globals added previously.")
+         :args "*"
+         :count "*"
+         :argname "<name>"
+         :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 "<name>"
+         :action "concat"
+         :init(nil),
+      parser:option("--not-globals", "Remove custom and standard global variables or fields.")
+         :args "*"
+         :count "*"
+         :argname "<name>"
+         :action "concat"
+         :init(nil),
+
+      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:group("Options for configuring line length limits",
+      parser:option("--max-line-length", "Set maximum allowed line length (default: 120).")
+         :argname "<length>"
+         :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 "<length>"
+         :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 "<length>"
+         :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 "<length>"
+         :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("--max-cyclomatic-complexity", "Set maximum cyclomatic complexity for functions.")
+      :argname "<complexity>"
+      :convert(tonumber)
+   parser:flag("--no-max-cyclomatic-complexity", "Do not limit function cyclomatic complexity (default).")
+      :action "store_false"
+      :target "max_cyclomatic_complexity"
+
+   local default_global_path = config.get_default_global_path()
+
+   local config_opt = parser:option("--config", "Path to configuration file. (default: "..config.default_path..")")
+   local no_config_opt = parser:flag("--no-config", "Do not look up configuration file.")
+      :action "store_false"
+      :target "config"
+
+   parser:mutex(config_opt, no_config_opt)
+
+   local default_config_opt = 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"))
+   local no_default_config_opt = parser:flag("--no-default-config", "Do not use default configuration file.")
+      :action "store_false"
+      :target "default_config"
+
+   parser:mutex(default_config_opt, no_default_config_opt)
+
+   parser:group("Configuration file options",
+      config_opt,
+      no_config_opt,
+      default_config_opt,
+      no_default_config_opt)
+
+   parser:group("File filtering options",
+      parser:option("--exclude-files", "Do not check files matching these globbing patterns.")
+         :args "+"
+         :count "*"
+         :argname "<glob>"
+         :action "concat"
+         :init(nil),
+      parser:option("--include-files", "Do not check files not matching these globbing patterns.")
+         :args "+"
+         :count "*"
+         :argname "<glob>"
+         :action "concat"
+         :init(nil))
+
+   parser:option("--filename", "Use another filename in output and for selecting configuration overrides.")
+
+   local cache_opt = parser:option("--cache", ("Path to cache directory. (default: %s)"):format(
+         cache.get_default_dir()))
+      :args "?"
+
+   local no_cache_opt = parser:flag("--no-cache", "Do not use cache.")
+      :action "store_false"
+      :target "cache"
+
+   parser:mutex(cache_opt, no_cache_opt)
+
+   local lanes_notice = ""
+
+   if not multithreading.has_lanes then
+      lanes_notice = "\nWarning: LuaLanes not found, parallel checking disabled."
+   end
+
+   parser:group("Performance optimization options",
+      cache_opt,
+      no_cache_opt,
+      parser:option(
+         "-j --jobs", "Check <jobs> files in parallel (default: " ..
+            tostring(multithreading.default_jobs) .. ")." .. lanes_notice):convert(tonumber))
+
+   parser:group("Output formatting options",
+      parser:option("--formatter" , "Use custom formatter. <formatter> must be a module name or one of:\n" ..
+         "   TAP - Test Anything Protocol formatter;\n" ..
+         "   JUnit - JUnit XML formatter;\n" ..
+         "   visual_studio - MSBuild/Visual Studio aware formatter;\n" ..
+         "   plain - simple warning-per-line formatter;\n" ..
+         "   default - standard formatter."),
+      parser:flag("-q --quiet", "Suppress output for files without warnings.\n" ..
+         "-qq: Suppress output of warnings.\n" ..
+         "-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.")
+         :action "store_false"
+         :target "color")
+
+   parser:flag("--profile", "Show performance statistics."):hidden(true)
+
+   parser:flag("-v --version", "Show version info and exit.")
+      :action(function() print(version.string) os.exit(exit_codes.ok) end)
+
+   return parser
+end
+
+local function main()
+   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
+
+   if args.profile then
+      args.jobs = 1
+      profiler.init()
+   end
+
+   if args.quiet == 0 then
+      args.quiet = nil
+   end
+
+   if args.cache then
+      args.cache = args.cache[1] or true
+   end
+
+   local checker, err, is_invalid_args_error = runner.new(args)
+
+   if not checker then
+      if is_invalid_args_error then
+         io.stderr:write(("%s\n\nError: %s\n"):format(parser:get_usage(), err))
+         os.exit(exit_codes.critical)
+      else
+         critical(err)
+      end
+   end
+
+   local inputs = {}
+
+   for _, file in ipairs(args.files) do
+      local input = {filename = args.filename}
+
+      if file == "-" then
+         input.file = io.stdin
+      elseif file:find("%.rockspec$") then
+         input.rockspec_path = file
+      else
+         input.path = file
+      end
+
+      table.insert(inputs, input)
+   end
+
+   local report, check_err = checker:check(inputs)
+
+   if not report then
+      critical(check_err)
+   end
+
+   for _, file_report in ipairs(report) do
+      if not file_report.filename then
+         file_report.filename = "stdin"
+      end
+   end
+
+   local output, format_err = checker:format(report)
+
+   if not output then
+      critical(format_err)
+   end
+
+   io.stdout:write(output)
+
+   if args.profile then
+      profiler.report()
+   end
+
+   if report.fatals > 0 then
+      os.exit(exit_codes.fatals)
+   elseif report.errors > 0 then
+      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:find("interrupted!$") then
+   critical("Interrupted")
+else
+   local msg = ("Luacheck %s bug (please report at https://github.com/mpeterv/luacheck/issues):\n%s\n%s"):format(
+      luacheck._VERSION, err, traceback)
+   critical(msg)
+end
diff --git a/script/luacheck/multithreading.lua b/script/luacheck/multithreading.lua
new file mode 100755
index 000000000..55df71657
--- /dev/null
+++ b/script/luacheck/multithreading.lua
@@ -0,0 +1,105 @@
+local 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 = jobs or multithreading.default_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
diff --git a/script/luacheck/options.lua b/script/luacheck/options.lua
new file mode 100755
index 000000000..991eb7e39
--- /dev/null
+++ b/script/luacheck/options.lua
@@ -0,0 +1,419 @@
+local 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 number_or_false = utils.has_type_or_false("number")
+local array_of_strings = utils.array_of("string")
+
+-- Validates std string.
+-- Returns an array of std names with `add` field if there is `+` at the beginning of the string.
+-- On validation error returns `nil` and an error message.
+local function split_std(std, stds)
+   local parts = utils.split(std, "+")
+
+   if parts[1]:match("^%s*$") then
+      parts.add = true
+      table.remove(parts, 1)
+   end
+
+   for i, part in ipairs(parts) do
+      parts[i] = utils.strip(part)
+
+      if not stds[parts[i]] then
+         return nil, ("unknown std '%s'"):format(parts[i])
+      end
+   end
+
+   return parts
+end
+
+local function std_or_array_of_strings(x, stds)
+   if type(x) == "string" then
+      local ok, err = split_std(x, stds)
+      return not not ok, err
+   elseif type(x) == "table" then
+      return standards.validate_std_table(x)
+   else
+      return false, "string or table expected, got " .. type(x)
+   end
+end
+
+local function field_map(x)
+   if type(x) == "table" then
+      return standards.validate_globals_table(x)
+   else
+      return false, "table expected, got " .. type(x)
+   end
+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,
+   max_cyclomatic_complexity = number_or_false
+}
+
+utils.update(options.all_options, options.nullary_inline_options)
+utils.update(options.all_options, options.variadic_inline_options)
+
+-- Returns true if opts is valid option_set or is nil.
+-- Otherwise returns false and an error message.
+function options.validate(option_set, opts, stds)
+   if opts == nil then
+      return true
+   end
+
+   if type(opts) ~= "table" then
+      return false, "option table expected, got " .. type(opts)
+   end
+
+   stds = stds or builtin_standards
+
+   for option, validator in utils.sorted_pairs(option_set) do
+      if opts[option] ~= nil then
+         local ok, err = validator(opts[option], stds)
+
+         if not ok then
+            return false, ("invalid value of option '%s': %s"):format(option, err)
+         end
+      end
+   end
+
+   return true
+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, stds)
+   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 = stds.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 = split_std(opts.std, stds)
+
+            for _, part in ipairs(parts) do
+               table.insert(add_stds, stds[part])
+            end
+
+            if not parts.add then
+               base_std = {}
+               break
+            end
+         end
+      end
+   end
+
+   table.insert(add_stds, 1, base_std or stds.max)
+   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, stds)
+   local final_std = {}
+   local std_tables = get_std_tables(opts_stack, stds)
+
+   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,
+   module = false,
+   allow_defined = false,
+   allow_defined_top = false,
+   max_cyclomatic_complexity = false
+}
+
+-- Returns normalized options.
+-- Normalized options have fields:
+--    std: normalized std table, see `luacheck.standards` module;
+--    unused_secondaries, self, module, allow_defined, allow_defined_top: booleans;
+--    max_line_length: number or false;
+--    rules: see get_rules.
+function options.normalize(opts_stack, stds)
+   local res = {}
+   stds = stds or builtin_standards
+   res.std = get_final_std(opts_stack, stds)
+
+   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
diff --git a/script/luacheck/parser.lua b/script/luacheck/parser.lua
new file mode 100755
index 000000000..744684156
--- /dev/null
+++ b/script/luacheck/parser.lua
@@ -0,0 +1,984 @@
+local lexer = require "luacheck.lexer"
+local utils = require "luacheck.utils"
+
+local parser = {}
+
+-- A table with range info, or simply range, has `line`, `offset`, and `end_offset` fields.
+-- `line` is the line of the first character.
+-- Parser state table has range info for the current token, and all AST
+-- node tables have range info for themself, including parens around expressions
+-- that are otherwise not reflected in the AST structure.
+
+parser.SyntaxError = utils.class()
+
+function parser.SyntaxError:__init(msg, range, prev_range)
+   self.msg = msg
+   self.line = range.line
+   self.offset = range.offset
+   self.end_offset = range.end_offset
+
+   if prev_range then
+      self.prev_line = prev_range.line
+      self.prev_offset = prev_range.offset
+      self.prev_end_offset = prev_range.end_offset
+   end
+end
+
+function parser.syntax_error(msg, range, prev_range)
+   error(parser.SyntaxError(msg, range, prev_range), 0)
+end
+
+local function mark_line_endings(state, token_type)
+   for line = state.line, state.lexer.line - 1 do
+      state.line_endings[line] = token_type
+   end
+end
+
+local function skip_token(state)
+   while true do
+      local token, token_value, line, offset, error_end_offset = lexer.next_token(state.lexer)
+      state.token = token
+      state.token_value = token_value
+      state.line = line
+      state.offset = offset
+      state.end_offset = error_end_offset or (state.lexer.offset - 1)
+
+      if not token then
+         parser.syntax_error(token_value, state)
+      end
+
+      if token == "short_comment" then
+         state.comments[#state.comments + 1] = {
+            contents = token_value,
+            line = line,
+            offset = offset,
+            end_offset = state.end_offset
+         }
+
+         state.line_endings[line] = "comment"
+      elseif token == "long_comment" then
+         mark_line_endings(state, "comment")
+      else
+         if token ~= "eof" then
+            mark_line_endings(state, "string")
+            state.code_lines[line] = true
+            state.code_lines[state.lexer.line] = true
+         end
+
+         return
+      end
+   end
+end
+
+local function token_name(token)
+   return token == "name" and "identifier" or (token == "eof" and "<eof>" or ("'" .. token .. "'"))
+end
+
+local function parse_error(state, msg, prev_range, token_prefix, message_suffix)
+   local token_repr
+
+   if state.token == "eof" then
+      token_repr = "<eof>"
+   else
+      token_repr = lexer.get_quoted_substring_or_line(state.lexer, state.line, state.offset, state.end_offset)
+   end
+
+   if token_prefix then
+      token_repr = token_prefix .. " " .. token_repr
+   end
+
+   msg = msg .. " near " .. token_repr
+
+   if message_suffix then
+      msg = msg .. " " .. message_suffix
+   end
+
+   parser.syntax_error(msg, state, prev_range)
+end
+
+local function check_token(state, token)
+   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 copy_range(range)
+   return {
+      line = range.line,
+      offset = range.offset,
+      end_offset = range.end_offset
+   }
+end
+
+local new_state
+local parse_block
+local missing_closing_token_error
+
+-- Attempt to guess a better location for missing `end` and `until` errors (usually they uselessly point to eof).
+-- Guessed error token should be selected in such a way that inserting previously missing closing token
+-- in front of it should fix the error or at least move its opening token forward.
+-- The idea is to track the stack of opening tokens and their indentations.
+-- For the first statement or closing token with the same or smaller indentation than the opening token
+-- on the top of the stack:
+-- * If it has the same indentation but is not the appropriate closing token for the opening one, pick it
+--   as the guessed error location.
+-- * If it has a lower indentation level, pick it as the guessed error location even it closes the opening token.
+-- Example:
+-- local function f()
+--    <code>
+--
+--    if cond then                   <- `if` is the guessed opening token
+--       <code>
+--
+--    <code not starting with `end`> <- first token on this line is the guessed error location
+-- end
+-- Another one:
+-- local function g()
+--    <code>
+--
+--    if cond then  <- `if` is the guessed opening token
+--       <code>
+--
+-- end              <- `end` is the guessed error location
+
+local opening_token_to_closing = {
+   ["("] = ")",
+   ["["] = "]",
+   ["{"] = "}",
+   ["do"] = "end",
+   ["if"] = "end",
+   ["else"] = "end",
+   ["elseif"] = "end",
+   ["while"] = "end",
+   ["repeat"] = "until",
+   ["for"] = "end",
+   ["function"] = "end"
+}
+
+local function get_indentation(state, line)
+   local ws_start, ws_end = state.lexer.src:find("^[ \t\v\f]*", state.lexer.line_offsets[line])
+   return ws_end - ws_start
+end
+
+local UnpairedTokenGuesser = utils.class()
+
+function UnpairedTokenGuesser:__init(state, error_opening_range, error_closing_token)
+   self.old_state = state
+   self.error_offset = state.offset
+   self.error_opening_range = error_opening_range
+   self.error_closing_token = error_closing_token
+   self.opening_tokens_stack = utils.Stack()
+end
+
+function UnpairedTokenGuesser:guess()
+   -- Need to reinitialize lexer (e.g. to skip shebang again).
+   self.state = new_state(self.old_state.lexer.src)
+   self.state.unpaired_token_guesser = self
+   skip_token(self.state)
+   parse_block(self.state)
+   error("No syntax error in second parse", 0)
+end
+
+function UnpairedTokenGuesser:on_block_start(opening_token_range, opening_token)
+   local token_wrapper = copy_range(opening_token_range)
+   token_wrapper.token = opening_token
+   token_wrapper.closing_token = opening_token_to_closing[opening_token]
+   token_wrapper.eligible = token_wrapper.closing_token == self.error_closing_token
+   token_wrapper.indentation = get_indentation(self.state, opening_token_range.line)
+   self.opening_tokens_stack:push(token_wrapper)
+end
+
+function UnpairedTokenGuesser:set_guessed()
+   -- Keep the first detected location.
+   if self.guessed then
+      return
+   end
+
+   self.guessed = self.opening_tokens_stack.top
+   self.guessed.error_token = self.state.token
+   self.guessed.error_range = copy_range(self.state)
+end
+
+function UnpairedTokenGuesser:check_token()
+   local top = self.opening_tokens_stack.top
+
+   if top and top.eligible and self.state.line > top.line then
+      local token_indentation = get_indentation(self.state, self.state.line)
+
+      if token_indentation < top.indentation then
+         self:set_guessed()
+      elseif token_indentation == top.indentation then
+         local token = self.state.token
+
+         if token ~= top.closing_token and
+               ((top.token ~= "if" and top.token ~= "elseif") or (token ~= "elseif" and token ~= "else")) then
+            self:set_guessed()
+         end
+      end
+   end
+
+   if self.state.offset == self.error_offset then
+      if self.guessed and self.guessed.error_range.offset ~= self.state.offset then
+         self.state.line = self.guessed.error_range.line
+         self.state.offset = self.guessed.error_range.offset
+         self.state.end_offset = self.guessed.error_range.end_offset
+         self.state.token = self.guessed.error_token
+         missing_closing_token_error(self.state, self.guessed, self.guessed.token, self.guessed.closing_token, true)
+      end
+   end
+end
+
+function UnpairedTokenGuesser:on_block_end()
+   self:check_token()
+   self.opening_tokens_stack:pop()
+
+   if not self.opening_tokens_stack.top then
+      -- Inserting an end token into a balanced sequence of tokens adds an error earlier than original one.
+      self.guessed = nil
+   end
+end
+
+function UnpairedTokenGuesser:on_statement()
+   self:check_token()
+end
+
+function missing_closing_token_error(state, opening_range, opening_token, closing_token, is_guess)
+   local msg = "expected " .. token_name(closing_token)
+
+   if opening_range and opening_range.line ~= state.line then
+      msg = msg .. " (to close " .. token_name(opening_token) .. " on line " .. tostring(opening_range.line) .. ")"
+   end
+
+   local token_prefix
+   local message_suffix
+
+   if is_guess then
+      if state.token == closing_token then
+         -- "expected 'end' near 'end'" seems confusing.
+         token_prefix = "less indented"
+      end
+
+      message_suffix = "(indentation-based guess)"
+   end
+
+   parse_error(state, msg, opening_range, token_prefix, message_suffix)
+end
+
+local function check_closing_token(state, opening_range, opening_token)
+   local closing_token = opening_token_to_closing[opening_token] or "eof"
+
+   if state.token == closing_token then
+      return
+   end
+
+   if (opening_token == "if" or opening_token == "elseif") and (state.token == "else" or state.token == "elseif") then
+      return
+   end
+
+   if closing_token == "end" or closing_token == "until" then
+      if not state.unpaired_token_guesser then
+         UnpairedTokenGuesser(state, opening_range, closing_token):guess()
+      end
+   end
+
+   missing_closing_token_error(state, opening_range, opening_token, closing_token)
+end
+
+local function check_and_skip_closing_token(state, opening_range, opening_token)
+   check_closing_token(state, opening_range, opening_token)
+   skip_token(state)
+end
+
+local function check_name(state)
+   check_token(state, "name")
+   return state.token_value
+end
+
+local function new_outer_node(range, tag, node)
+   node = node or {}
+   node.line = range.line
+   node.offset = range.offset
+   node.end_offset = range.end_offset
+   node.tag = tag
+   return node
+end
+
+local function new_inner_node(start_range, end_range, tag, node)
+   node = node or {}
+   node.line = start_range.line
+   node.offset = start_range.offset
+   node.end_offset = end_range.end_offset
+   node.tag = tag
+   return node
+end
+
+local parse_expression
+
+local function parse_expression_list(state, list)
+   list = list or {}
+
+   repeat
+      list[#list + 1] = parse_expression(state)
+   until not test_and_skip_token(state, ",")
+
+   return list
+end
+
+local function parse_id(state, tag)
+   local ast_node = new_outer_node(state, tag or "Id")
+   ast_node[1] = check_name(state)
+   -- Skip name.
+   skip_token(state)
+   return ast_node
+end
+
+local function atom(tag)
+   return function(state)
+      local ast_node = new_outer_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 start_range = copy_range(state)
+   local ast_node = {}
+   skip_token(state)
+
+   repeat
+      if state.token == "}" then
+         break
+      end
+
+      local key_node, value_node
+      local first_token_range = copy_range(state)
+
+      if state.token == "name" then
+         local name = state.token_value
+         skip_token(state)  -- Skip name.
+
+         if test_and_skip_token(state, "=") then
+            -- `name` = `expr`.
+            key_node = new_outer_node(first_token_range, "String", {name})
+            value_node = parse_expression(state)
+         else
+            -- `name` is beginning of an expression in array part.
+            -- Backtrack lexer to before name.
+            state.lexer.line = first_token_range.line
+            state.lexer.offset = first_token_range.offset
+            skip_token(state)  -- Load name again.
+            value_node = parse_expression(state)
+         end
+      elseif state.token == "[" then
+         -- [ `expr` ] = `expr`.
+         skip_token(state)
+         key_node = parse_expression(state)
+         check_and_skip_closing_token(state, first_token_range, "[")
+         check_and_skip_token(state, "=")
+         value_node = parse_expression(state)
+      else
+         -- Expression in array part.
+         value_node = parse_expression(state)
+      end
+
+      if key_node then
+         -- Pair.
+         ast_node[#ast_node + 1] = new_inner_node(first_token_range, value_node, "Pair", {key_node, value_node})
+      else
+         -- Array part item.
+         ast_node[#ast_node + 1] = value_node
+      end
+   until not (test_and_skip_token(state, ",") or test_and_skip_token(state, ";"))
+
+   new_inner_node(start_range, state, "Table", ast_node)
+   check_and_skip_closing_token(state, start_range, "{")
+   return ast_node
+end
+
+-- Parses argument list and the statements.
+local function parse_function(state, function_range)
+   local paren_range = copy_range(state)
+   check_and_skip_token(state, "(")
+   local args = {}
+
+   -- Are there arguments?
+   if state.token ~= ")" then
+      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_and_skip_closing_token(state, paren_range, "(")
+   local body = parse_block(state, function_range, "function")
+   local end_range = copy_range(state)
+   -- Skip "function".
+   skip_token(state)
+   return new_inner_node(function_range, end_range, "Function", {args, body, end_range = end_range})
+end
+
+simple_expressions["function"] = function(state)
+   local function_range = copy_range(state)
+   -- Skip "function".
+   skip_token(state)
+   return parse_function(state, function_range)
+end
+
+-- A call handler parses arguments of a call with given base node that determines resulting node start location,
+-- given tag, and array to which the arguments should be appended.
+local call_handlers = {}
+
+call_handlers["("] = function(state, base_node, tag, node)
+   local paren_range = copy_range(state)
+   -- Skip "(".
+   skip_token(state)
+
+   if state.token ~= ")" then
+      parse_expression_list(state, node)
+   end
+
+   new_inner_node(base_node, state, tag, node)
+   check_and_skip_closing_token(state, paren_range, "(")
+   return node
+end
+
+call_handlers["{"] = function(state, base_node, tag, node)
+   local arg_node = simple_expressions[state.token](state)
+   node[#node + 1] = arg_node
+   return new_inner_node(base_node, arg_node, tag, node)
+end
+
+call_handlers.string = call_handlers["{"]
+
+local suffix_handlers = {}
+
+suffix_handlers["."] = function(state, base_node)
+   -- Skip ".".
+   skip_token(state)
+   local index_node = parse_id(state, "String")
+   return new_inner_node(base_node, index_node, "Index", {base_node, index_node})
+end
+
+suffix_handlers["["] = function(state, base_node)
+   local bracket_range = copy_range(state)
+   -- Skip "[".
+   skip_token(state)
+   local index_node = parse_expression(state)
+   local ast_node = new_inner_node(base_node, state, "Index", {base_node, index_node})
+   check_and_skip_closing_token(state, bracket_range, "[")
+   return ast_node
+end
+
+suffix_handlers[":"] = function(state, base_node)
+   -- Skip ":".
+   skip_token(state)
+   local method_name = parse_id(state, "String")
+   local call_handler = call_handlers[state.token]
+
+   if not call_handler then
+      parse_error(state, "expected method arguments")
+   end
+
+   return call_handler(state, base_node, "Invoke", {base_node, method_name})
+end
+
+suffix_handlers["("] = function(state, base_node)
+   return call_handlers[state.token](state, base_node, "Call", {base_node})
+end
+
+suffix_handlers["{"] = suffix_handlers["("]
+suffix_handlers.string = suffix_handlers["("]
+
+local function parse_simple_expression(state, kind, no_literals)
+   local expression
+
+   if state.token == "(" then
+      local paren_range = copy_range(state)
+      skip_token(state)
+      local inner_expression = parse_expression(state)
+      expression = new_inner_node(paren_range, state, "Paren", {inner_expression})
+      check_and_skip_closing_token(state, paren_range, "(")
+   elseif state.token == "name" then
+      expression = parse_id(state)
+   else
+      local literal_handler = simple_expressions[state.token]
+
+      if not literal_handler or no_literals then
+         parse_error(state, "expected " .. (kind or "expression"))
+      end
+
+      return literal_handler(state)
+   end
+
+   while true do
+      local suffix_handler = suffix_handlers[state.token]
+
+      if suffix_handler then
+         expression = suffix_handler(state, expression)
+      else
+         return expression
+      end
+   end
+end
+
+local unary_operators = {
+   ["not"] = "not",
+   ["-"] = "unm",
+   ["~"] = "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
+}
+
+local function parse_subexpression(state, limit, kind)
+   local expression
+   local unary_operator = unary_operators[state.token]
+
+   if unary_operator then
+      local operator_range = copy_range(state)
+      -- Skip operator.
+      skip_token(state)
+      local operand = parse_subexpression(state, unary_priority)
+      expression = new_inner_node(operator_range, operand, "Op", {unary_operator, operand})
+   else
+      expression = 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
+
+       -- Skip operator.
+      skip_token(state)
+      -- Read subexpression with higher priority.
+      local subexpression = parse_subexpression(state, right_priorities[binary_operator])
+      expression = new_inner_node(expression, subexpression, "Op", {binary_operator, expression, subexpression})
+   end
+
+   return expression
+end
+
+function parse_expression(state, kind)
+   return parse_subexpression(state, 0, kind)
+end
+
+local statements = {}
+
+statements["if"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "if".
+   skip_token(state)
+   local ast_node = {}
+
+   -- The loop is entered after skipping "if" or "elseif".
+   -- Block start token info is set to the last skipped "if", "elseif", or "else" token.
+   local block_start_token = "if"
+   local block_start_range = start_range
+
+   while true do
+      ast_node[#ast_node + 1] = parse_expression(state, "condition")
+      -- Add range of the "then" token to the block statement array.
+      local branch_range = copy_range(state)
+      check_and_skip_token(state, "then")
+      ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range)
+
+      if state.token == "else" then
+         branch_range = copy_range(state)
+         block_start_token = "else"
+         block_start_range = branch_range
+         skip_token(state)
+         ast_node[#ast_node + 1] = parse_block(state, block_start_range, block_start_token, branch_range)
+         break
+      elseif state.token == "elseif" then
+         block_start_token = "elseif"
+         block_start_range = copy_range(state)
+         skip_token(state)
+      else
+         break
+      end
+   end
+
+   new_inner_node(start_range, state, "If", ast_node)
+   -- Skip "end".
+   skip_token(state)
+   return ast_node
+end
+
+statements["while"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "while".
+   skip_token(state)
+   local condition = parse_expression(state, "condition")
+   check_and_skip_token(state, "do")
+   local block = parse_block(state, start_range, "while")
+   local ast_node = new_inner_node(start_range, state, "While", {condition, block})
+   -- Skip "end".
+   skip_token(state)
+   return ast_node
+end
+
+statements["do"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "do".
+   skip_token(state)
+   local block = parse_block(state, start_range, "do")
+   local ast_node = new_inner_node(start_range, state, "Do", block)
+   -- Skip "end".
+   skip_token(state)
+   return ast_node
+end
+
+statements["for"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "for".
+   skip_token(state)
+
+   local ast_node = {}
+   local tag
+   local first_var = parse_id(state)
+
+   if state.token == "=" then
+      -- Numeric "for" loop.
+      tag = "Fornum"
+      -- Skip "=".
+      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, start_range, "for")
+   elseif state.token == "," or state.token == "in" then
+      -- Generic "for" loop.
+      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, start_range, "for")
+   else
+      parse_error(state, "expected '=', ',' or 'in'")
+   end
+
+   new_inner_node(start_range, state, tag, ast_node)
+   -- Skip "end".
+   skip_token(state)
+   return ast_node
+end
+
+statements["repeat"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "repeat".
+   skip_token(state)
+   local block = parse_block(state, start_range, "repeat")
+   -- Skip "until".
+   skip_token(state)
+   local condition = parse_expression(state, "condition")
+   return new_inner_node(start_range, condition, "Repeat", {block, condition})
+end
+
+statements["function"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "function".
+   skip_token(state)
+   local lhs = parse_id(state)
+   local implicit_self_range
+
+   while (not implicit_self_range) and (state.token == "." or state.token == ":") do
+      implicit_self_range = (state.token == ":") and copy_range(state)
+      -- Skip "." or ":".
+      skip_token(state)
+      local index_node = parse_id(state, "String")
+      lhs = new_inner_node(lhs, index_node, "Index", {lhs, index_node})
+   end
+
+   local function_node = parse_function(state, start_range)
+
+   if implicit_self_range then
+      -- Insert implicit "self" argument.
+      local self_arg = new_outer_node(implicit_self_range, "Id", {"self", implicit = true})
+      table.insert(function_node[1], 1, self_arg)
+   end
+
+   return new_inner_node(start_range, function_node, "Set", {{lhs}, {function_node}})
+end
+
+statements["local"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "local".
+   skip_token(state)
+
+   if state.token == "function" then
+      -- Local function.
+      local function_range = copy_range(state)
+      -- Skip "function".
+      skip_token(state)
+      local var = parse_id(state)
+      local function_node = parse_function(state, function_range)
+      return new_inner_node(start_range, function_node, "Localrec", {{var}, {function_node}})
+   end
+
+   -- Local definition, potentially with assignment.
+   local lhs = {}
+   local rhs
+
+   repeat
+      lhs[#lhs + 1] = parse_id(state)
+   until not test_and_skip_token(state, ",")
+
+   if test_and_skip_token(state, "=") then
+      rhs = parse_expression_list(state)
+   end
+
+   return new_inner_node(start_range, rhs and rhs[#rhs] or lhs[#lhs], "Local", {lhs, rhs})
+end
+
+statements["::"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "::".
+   skip_token(state)
+   local name = check_name(state)
+   -- Skip label name.
+   skip_token(state)
+   local ast_node = new_inner_node(start_range, state, "Label", {name})
+   check_and_skip_token(state, "::")
+   return ast_node
+end
+
+local closing_tokens = utils.array_to_set({"end", "eof", "else", "elseif", "until"})
+
+statements["return"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "return".
+   skip_token(state)
+
+   if closing_tokens[state.token] or state.token == ";" then
+      -- No return values.
+      return new_outer_node(start_range, "Return")
+   else
+      local returns = parse_expression_list(state)
+      return new_inner_node(start_range, returns[#returns], "Return", returns)
+   end
+end
+
+statements["break"] = function(state)
+   local ast_node = new_outer_node(state, "Break")
+   -- Skip "break".
+   skip_token(state)
+   return ast_node
+end
+
+statements["goto"] = function(state)
+   local start_range = copy_range(state)
+   -- Skip "goto".
+   skip_token(state)
+   local name = check_name(state)
+   local ast_node = new_outer_node(start_range, "Goto", {name})
+   -- Skip label name.
+   skip_token(state)
+   return ast_node
+end
+
+local function parse_expression_statement(state)
+   local lhs
+   local start_range = copy_range(state)
+
+   -- Handle lhs of an assignment or a single expression.
+   repeat
+      local item_start_range = lhs and copy_range(state) or start_range
+      local expected = lhs and "identifier or field" or "statement"
+      local primary_expression = parse_simple_expression(state, expected, true)
+
+      if primary_expression.tag == "Paren" then
+         -- (expr) in lhs is invalid.
+         parser.syntax_error("expected " .. expected .. " near '('", item_start_range)
+      end
+
+      if primary_expression.tag == "Call" or primary_expression.tag == "Invoke" then
+         if lhs then
+            -- The is an assingment, and a call is not valid in lhs.
+            parse_error(state, "expected call or indexing")
+         else
+            -- This is a call.
+            return primary_expression
+         end
+      end
+
+      -- This is an assignment.
+      lhs = lhs or {}
+      lhs[#lhs + 1] = primary_expression
+   until not test_and_skip_token(state, ",")
+
+   check_and_skip_token(state, "=")
+   local rhs = parse_expression_list(state)
+   return new_inner_node(start_range, rhs[#rhs], "Set", {lhs, rhs})
+end
+
+local function parse_statement(state)
+   return (statements[state.token] or parse_expression_statement)(state)
+end
+
+function parse_block(state, opening_token_range, opening_token, block)
+   local unpaired_token_guesser = state.unpaired_token_guesser
+
+   if unpaired_token_guesser and opening_token then
+      unpaired_token_guesser:on_block_start(opening_token_range, opening_token)
+   end
+
+   block = block or {}
+   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, copy_range(state))
+         end
+
+         -- Skip ";".
+         skip_token(state)
+         -- Further semicolons are considered hanging.
+         after_statement = false
+      else
+         if unpaired_token_guesser then
+            unpaired_token_guesser:on_statement()
+         end
+
+         local statement = parse_statement(state)
+         after_statement = true
+         block[#block + 1] = statement
+
+         if statement.tag == "Return" then
+            -- "return" must be the last statement.
+            -- However, one ";" after it is allowed.
+            test_and_skip_token(state, ";")
+            break
+         end
+      end
+   end
+
+   if unpaired_token_guesser and opening_token then
+      unpaired_token_guesser:on_block_end()
+   end
+
+   check_closing_token(state, opening_token_range, opening_token)
+
+   return block
+end
+
+function new_state(src, line_offsets, line_lengths)
+   return {
+      lexer = lexer.new_state(src, line_offsets, line_lengths),
+      -- Set of line numbers containing code.
+      code_lines = {},
+      -- Maps line numbers to "comment", "string", or nil based on whether the line ending is within a token
+      line_endings = {},
+      -- Array of {contents = string} with range info.
+      comments = {},
+       -- Array of ranges of semicolons not following a statement.
+      hanging_semicolons = {}
+   }
+end
+
+-- Parses source characters.
+-- Returns AST (in almost MetaLua format), array of comments - tables {contents = string} with range info,
+-- set of line numbers containing code, map of types of tokens wrapping line endings (nil, "string", or "comment"),
+-- array of ranges of hanging semicolons (not after statements), array of line start offsets, array of line lengths.
+-- The last two tables can be passed as arguments to be filled.
+-- On error throws an instance of parser.SyntaxError: table {msg = msg, prev_range = prev_range?} with range info,
+-- prev_range may refer to some extra relevant location.
+function parser.parse(src, line_offsets, line_lengths)
+   local state = new_state(src, line_offsets, line_lengths)
+   skip_token(state)
+   local ast = parse_block(state)
+   return ast, state.comments, state.code_lines, state.line_endings, state.hanging_semicolons,
+      state.lexer.line_offsets, state.lexer.line_lengths
+end
+
+return parser
diff --git a/script/luacheck/profiler.lua b/script/luacheck/profiler.lua
new file mode 100755
index 000000000..37104a799
--- /dev/null
+++ b/script/luacheck/profiler.lua
@@ -0,0 +1,149 @@
+-- Require luasocket only when needed.
+local socket
+
+local profiler = {}
+
+local metrics = {
+   {name = "Wall", get = function() return socket.gettime() end},
+   {name = "CPU", get = os.clock},
+   {name = "Memory", get = function() return collectgarbage("count") end}
+}
+
+local functions = {
+   {name = "sha1", module = "vendor.sha1"},
+   {name = "load", module = "cache"},
+   {name = "update", module = "cache"},
+   {name = "decode", module = "decoder"},
+   {name = "parse", module = "parser"},
+   {name = "dump_check_result", module = "serializer"},
+   {name = "load_check_result", module = "serializer"},
+   {name = "run", module = "stages.unwrap_parens"},
+   {name = "run", module = "stages.parse_inline_options"},
+   {name = "run", module = "stages.linearize"},
+   {name = "run", module = "stages.name_functions"},
+   {name = "run", module = "stages.resolve_locals"},
+   {name = "run", module = "stages.detect_bad_whitespace"},
+   {name = "run", module = "stages.detect_cyclomatic_complexity"},
+   {name = "run", module = "stages.detect_empty_blocks"},
+   {name = "run", module = "stages.detect_empty_statements"},
+   {name = "run", module = "stages.detect_globals"},
+   {name = "run", module = "stages.detect_reversed_fornum_loops"},
+   {name = "run", module = "stages.detect_unbalanced_assignments"},
+   {name = "run", module = "stages.detect_uninit_accesses"},
+   {name = "run", module = "stages.detect_unreachable_code"},
+   {name = "run", module = "stages.detect_unused_fields"},
+   {name = "run", module = "stages.detect_unused_locals"},
+   {name = "filter", module = "filter"},
+   {name = "normalize", module = "options"}
+}
+
+local stats = {}
+local start_values = {}
+
+local function start_phase(name)
+   for _, metric in ipairs(metrics) do
+      start_values[metric][name] = metric.get()
+   end
+end
+
+local function stop_phase(name)
+   for _, metric in ipairs(metrics) do
+      local increment = metric.get() - start_values[metric][name]
+      stats[metric][name] = (stats[metric][name] or 0) + increment
+   end
+end
+
+local phase_stack = {}
+
+local function push_phase(name)
+   local prev_name = phase_stack[#phase_stack]
+
+   if prev_name then
+      stop_phase(prev_name)
+   end
+
+   table.insert(phase_stack, name)
+   start_phase(name)
+end
+
+local function pop_phase(name)
+   assert(name == table.remove(phase_stack))
+   stop_phase(name)
+   local prev_name = phase_stack[#phase_stack]
+
+   if prev_name then
+      start_phase(prev_name)
+   end
+end
+
+local function continue_wrapper(name, ...)
+   pop_phase(name)
+   return ...
+end
+
+local function wrap(fn, name)
+   return function(...)
+      push_phase(name)
+      return continue_wrapper(name, fn(...))
+   end
+end
+
+local function patch(fn)
+   local mod = require("luacheck." .. fn.module)
+   local orig = mod[fn.name]
+   local new = wrap(orig, fn.module .. "." .. fn.name)
+   mod[fn.name] = new
+end
+
+function profiler.init()
+   socket = require "socket"
+   collectgarbage("stop")
+
+   for _, metric in ipairs(metrics) do
+      stats[metric] = {}
+      start_values[metric] = {}
+   end
+
+   for _, fn in ipairs(functions) do
+      patch(fn)
+   end
+
+   push_phase("other")
+end
+
+function profiler.report()
+   pop_phase("other")
+
+   for _, metric in ipairs(metrics) do
+      local names = {}
+      local total = 0
+
+      for name, value in pairs(stats[metric]) do
+         table.insert(names, name)
+         total = total + value
+      end
+
+      table.sort(names, function(name1, name2)
+         local stats1 = stats[metric][name1]
+         local stats2 = stats[metric][name2]
+
+         if stats1 ~= stats2 then
+            return stats1 > stats2
+         else
+            return name1 < name2
+         end
+      end)
+
+      print(metric.name)
+      print()
+
+      for _, name in ipairs(names) do
+         print(("%s - %f (%f%%)"):format(name, stats[metric][name], stats[metric][name] / total * 100))
+      end
+
+      print(("Total - %f"):format(total))
+      print()
+   end
+end
+
+return profiler
diff --git a/script/luacheck/runner.lua b/script/luacheck/runner.lua
new file mode 100755
index 000000000..ce46221dd
--- /dev/null
+++ b/script/luacheck/runner.lua
@@ -0,0 +1,415 @@
+local cache = require "luacheck.cache"
+local config = require "luacheck.config"
+local expand_rockspec = require "luacheck.expand_rockspec"
+local format = require "luacheck.format"
+local fs = require "luacheck.fs"
+local globbing = require "luacheck.globbing"
+local luacheck = require "luacheck"
+local multithreading = require "luacheck.multithreading"
+local options = require "luacheck.options"
+local utils = require "luacheck.utils"
+
+local runner = {}
+
+local Runner = utils.class()
+
+function Runner:__init(config_stack)
+   self._config_stack = config_stack
+end
+
+local config_options = {
+   config = utils.has_type_or_false("string"),
+   default_config = utils.has_type_or_false("string")
+}
+
+function runner.new(opts)
+   local ok, err = options.validate(config_options, opts)
+
+   if not ok then
+      error(("bad argument #1 to 'runner.new' (%s)"):format(err))
+   end
+
+   local base_config, config_err = config.load_config(opts.config, opts.default_config)
+
+   if not base_config then
+      return nil, config_err
+   end
+
+   local override_config = config.table_to_config(opts)
+
+   local config_stack
+   config_stack, err = config.stack_configs({base_config, override_config})
+
+   if not config_stack then
+      return nil, err
+   end
+
+   return Runner(config_stack)
+end
+
+local function validate_inputs(inputs)
+   if type(inputs) ~= "table" then
+      return nil, ("inputs table expected, got %s"):format(inputs)
+   end
+
+   for index, input in ipairs(inputs) do
+      local context = ("invalid input table at index [%d]"):format(index)
+
+      if type(input) ~= "table" then
+         return nil, ("%s: table expected, got %s"):format(context, type(input))
+      end
+
+      local specifies_source
+
+      for _, field in ipairs({"file", "filename", "path", "rockspec_path", "string"}) do
+         if input[field] ~= nil then
+            if field == "file" then
+               if io.type(input[field]) ~= "file" then
+                  return nil, ("%s: invalid field 'file': open file expected, got %s"):format(
+                     context, type(input[field]))
+               end
+            elseif type(input[field]) ~= "string" then
+               return nil, ("%s: invalid field '%s': string expected, got %s"):format(
+                  context, field, type(input[field]))
+            end
+
+            if field ~= "filename" then
+               specifies_source = true
+            end
+         end
+      end
+
+      if not specifies_source then
+         return nil, ("%s: one of fields 'path', 'rockspec_path', 'file', or 'string' must be present"):format(context)
+      end
+   end
+
+   return true
+end
+
+local function matches_any(globs, filename)
+   for _, glob in ipairs(globs) do
+      if globbing.match(glob, filename) then
+         return true
+      end
+   end
+
+   return false
+end
+
+function Runner:_is_filename_included(abs_filename)
+   return not matches_any(self._top_opts.exclude_files, abs_filename) and (
+      #self._top_opts.include_files == 0 or matches_any(self._top_opts.include_files, abs_filename))
+end
+
+-- Normalizes inputs and filters inputs using `exclude_files` and `include_files` options.
+-- Returns an array of prepared input tables.
+-- Differences between normal and prepated inputs:
+-- * Prepared inputs can't have `rockspec_path` field.
+-- * Prepared inputs can't have `path` pointing to a directory (unless it has an error).
+-- * Prepared inputs have `filename` field if possible (copied from `path` if not given).
+-- * Prepared inputs that have `path` field also have `abs_path` field.
+-- * Prepared inputs can have `fatal` field if the input can't be checked. The value is error type as a string.
+--   `fatal` is always accompanied by an error message in `msg` field.
+function Runner:_prepare_inputs(inputs)
+   local current_dir = fs.get_current_dir()
+   local dir_pattern = #self._top_opts.include_files > 0 and "" or "%.lua$"
+
+   local res = {}
+
+   local function add(input)
+      if input.path then
+         -- TODO: get rid of this, adjust fs.extract_files to avoid leading `./` instead.
+         input.path = input.path:gsub("^%.[/\\]([^/])", "%1")
+         input.abs_path = fs.normalize(fs.join(current_dir, input.path))
+      end
+
+      local abs_filename
+
+      if input.filename then
+         abs_filename = fs.normalize(fs.join(current_dir, input.filename))
+      else
+         input.filename = input.path
+         abs_filename = input.abs_path
+      end
+
+      if not input.filename or self:_is_filename_included(abs_filename) then
+         table.insert(res, input)
+      end
+   end
+
+   for _, input in ipairs(inputs) do
+      if input.path then
+         if fs.is_dir(input.path) then
+            local filenames, err_map = fs.extract_files(input.path, dir_pattern)
+
+            for _, filename in ipairs(filenames) do
+               local err = err_map[filename]
+               if err then
+                  add({path = filename, fatal = "I/O", msg = err, filename = input.filename})
+               else
+                  add({path = filename, filename = input.filename})
+               end
+            end
+         else
+            add({path = input.path, filename = input.filename})
+         end
+      elseif input.rockspec_path then
+         local filenames, fatal, err = expand_rockspec(input.rockspec_path)
+
+         if filenames then
+            for _, filename in ipairs(filenames) do
+               add({path = filename, filename = input.filename})
+            end
+         else
+            add({path = input.rockspec_path, fatal = fatal, msg = err, filename = input.filename})
+         end
+      elseif input.file then
+         add({file = input.file, filename = input.filename})
+      elseif input.string then
+         add({string = input.string, filename = input.filename})
+      else
+         -- Validation should ensure this never happens.
+         error("input doesn't specify source to check")
+      end
+   end
+
+   return res
+end
+
+-- Loads cached reports for inputs with `path` field, assigns them to `cached_report` field.
+-- For each file on cache load error sets its `fatal` and `msg` fields.
+function Runner:_add_cached_reports(inputs)
+   for _, input in ipairs(inputs) do
+      if not input.fatal and input.path then
+         local report, err = self._cache:get(input.path)
+
+         if err then
+            input.fatal = "I/O"
+            input.msg = ("Couldn't load cache for %s from %s: malformed data"):format(
+               self._top_opts.cache, input.path)
+         else
+            input.cached_report = report
+         end
+      end
+   end
+end
+
+-- Adds report as `new_report` field to all inputs that don't have a fatal error or a cached report.
+-- Adds `fatal` and `msg` instead if there was an I/O error.
+function Runner:_add_new_reports(inputs)
+   local sources = {}
+   local original_indexes = {}
+
+   for index, input in ipairs(inputs) do
+      if not input.fatal and not input.cached_report then
+         if input.string then
+            table.insert(sources, input.string)
+            table.insert(original_indexes, index)
+         else
+            local source, err = utils.read_file(input.path or input.file)
+
+            if source then
+               table.insert(sources, source)
+               table.insert(original_indexes, index)
+            else
+               input.fatal = "I/O"
+               input.msg = err
+            end
+         end
+      end
+   end
+
+   local map = multithreading.has_lanes and multithreading.pmap or utils.map
+   local reports = map(luacheck.get_report, sources, self._top_opts.jobs)
+
+   for index, report in ipairs(reports) do
+      inputs[original_indexes[index]].new_report = report
+   end
+end
+
+-- Saves `new_report` for files eligible for caching to cache.
+-- Returns true on success or nil and an error message on failure.
+function Runner:_save_new_reports_to_cache(inputs)
+   for _, input in ipairs(inputs) do
+      if input.new_report and input.path then
+         local ok = self._cache:put(input.path, input.new_report)
+
+         if not ok then
+            return nil, ("Couldn't save cache for %s from %s: I/O error"):format(input.path, self._top_opts.cache)
+         end
+      end
+   end
+
+   return true
+end
+
+-- Inputs are prepared here, see `Runner:_prepare_inputs`.
+-- Returns an array of reports, one per input, possibly annotated with fields `fatal`, `msg`, and `filename`.
+-- On critical error returns nil and an error message.
+function Runner:_get_reports(inputs)
+   if self._top_opts.cache then
+      local err
+      self._cache, err = cache.new(self._top_opts.cache)
+
+      if not self._cache then
+         return nil, err
+      end
+
+      self:_add_cached_reports(inputs)
+   end
+
+   self:_add_new_reports(inputs)
+
+   if self._top_opts.cache then
+      local ok, err = self:_save_new_reports_to_cache(inputs)
+
+      if not ok then
+         return nil, err
+      end
+   end
+
+   local res = {}
+
+   for _, input in ipairs(inputs) do
+      local report = input.cached_report or input.new_report
+
+      if not report then
+         report = {fatal = input.fatal, msg = input.msg}
+      end
+
+      report.filename = input.filename
+      table.insert(res, report)
+   end
+
+   return res
+end
+
+function Runner:_get_final_report(reports)
+   local processing_options = {}
+
+   for index, report in ipairs(reports) do
+      if not report.fatal then
+         processing_options[index] = self._config_stack:get_options(report.filename)
+      end
+   end
+
+   local final_report = luacheck.process_reports(reports, processing_options, self._config_stack:get_stds())
+
+   -- `luacheck.process_reports` doesn't preserve `filename` fields, re-add them.
+   -- TODO: make it preserve them?
+   for index, report in ipairs(reports) do
+      final_report[index].filename = report.filename
+   end
+
+   return final_report
+end
+
+-- Inputs is an array of tables, each one specifies an input.
+-- Each input table must have one of the following fields:
+-- * `path`: string pointing to a file or directory to check. Checking directories requires LuaFileSystem,
+--   and recursively checks all files within the directory. If `include_files` option is not used,
+--   only files with `.lua` extensions within the directory are considered.
+-- * `rockspec_path`: string pointing to a rockspec, all files with `.lua` extension within its `build.modules`,
+--   `build.install.lua`, and `build.install.bin` tables are checked.
+-- * `file`: an open file object. It is read till EOF and closed, contents are checked.
+-- * `string`: Lua code to check as a string.
+-- Additionally, each input table can have `filename` field: a string used when applying `exclude_files`
+-- and `include_files` options to the input, and also when figuring out which per-path option overrides to use.
+-- By default, if `path` field is given, it is also used as `filename`, otherwise the input is considered unnamed.
+-- Unnamed files always pass `exclude_files` and `include_files` filters and don't have any per-path options applied.
+function Runner:check(inputs)
+   local ok, err = validate_inputs(inputs)
+
+   if not ok then
+      error(("bad argument #1 to 'Runner:check' (%s)"):format(err))
+   end
+
+   -- Path-related top options can depend on current directory.
+   -- Assume it can't somehow change during `:check` call.
+   self._top_opts = self._config_stack:get_top_options()
+
+   local prepared_inputs = self:_prepare_inputs(inputs)
+   local reports, reports_err = self:_get_reports(prepared_inputs)
+
+   if not reports then
+      return nil, reports_err
+   end
+
+   return self:_get_final_report(reports)
+end
+
+-- Formats given report (same format as returned by `Runner:check`).
+-- Optionally a table of options can be passed as `format_opts`,
+-- it can contain options `formatter`. `quiet`, `color`, `codes`, and `ranges`,
+-- with priority over options from initialization and config.
+-- Returns formatted report as a string. It always has a newline at the end unless it is empty.
+-- On error returns nil and an error message.
+function Runner:format(report, format_opts)
+   if type(report) ~= "table" then
+      error(("bad argument #1 to 'Runner:format' (report table expected, got %s"):format(type(report)))
+   end
+
+   local is_valid, err = options.validate(config.format_options, format_opts)
+
+   if not is_valid then
+      error(("bad argument #2 to 'Runner:format' (%s)"):format(err))
+   end
+
+   local top_opts = self._config_stack:get_top_options()
+   format_opts = format_opts or {}
+
+   local combined_opts = {}
+
+   for _, option in ipairs({"formatter", "quiet", "color", "codes", "ranges"}) do
+      combined_opts[option] = top_opts[option]
+
+      if format_opts[option] ~= nil then
+         combined_opts[option] = format_opts[option]
+      end
+   end
+
+   local filenames = {}
+
+   for _, file_report in ipairs(report) do
+      table.insert(filenames, file_report.filename or "<unnamed source>")
+   end
+
+   local output
+
+   if format.builtin_formatters[combined_opts.formatter] then
+      output = format.format(report, filenames, combined_opts)
+   else
+      local formatter_func = combined_opts.formatter
+
+      if type(combined_opts.formatter) == "string" then
+         local require_ok
+         local formatter_anchor_dir
+
+         if not format_opts.formatter then
+            formatter_anchor_dir = top_opts.formatter_anchor_dir
+         end
+
+         require_ok, formatter_func = config.relative_require(formatter_anchor_dir, combined_opts.formatter)
+
+         if not require_ok then
+            return nil, ("Couldn't load custom formatter '%s': %s"):format(combined_opts.formatter, formatter_func)
+         end
+      end
+
+      local ok
+      ok, output = pcall(formatter_func, report, filenames, combined_opts)
+
+      if not ok then
+         return nil, ("Couldn't run custom formatter '%s': %s"):format(tostring(combined_opts.formatter), output)
+      end
+   end
+
+   if #output > 0 and output:sub(-1) ~= "\n" then
+      output = output .. "\n"
+   end
+
+   return output
+end
+
+return runner
diff --git a/script/luacheck/serializer.lua b/script/luacheck/serializer.lua
new file mode 100755
index 000000000..03e50142a
--- /dev/null
+++ b/script/luacheck/serializer.lua
@@ -0,0 +1,211 @@
+local parse_inline_options = require "luacheck.stages.parse_inline_options"
+local stages = require "luacheck.stages"
+local utils = require "luacheck.utils"
+
+local serializer = {}
+
+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",
+   "max_cyclomatic_complexity"
+}
+
+local function compress_table(t, fields)
+   local res = {}
+
+   for index, field in ipairs(fields) do
+      local value = t[field]
+
+      if value ~= nil then
+         if field == "options" then
+            value = compress_table(value, option_fields)
+         end
+
+         res[index] = value
+      end
+   end
+
+   return res
+end
+
+local function compress_tables(tables, per_code_fields)
+   local res = {}
+
+   for _, t in ipairs(tables) do
+      local fields = per_code_fields and stages.warnings[t.code].fields or parse_inline_options.inline_option_fields
+      table.insert(res, compress_table(t, fields))
+   end
+
+   return res
+end
+
+local function compress_result(result)
+   local res = {}
+   res[1] = compress_tables(result.warnings, true)
+   res[2] = compress_tables(result.inline_options)
+   res[3] = result.line_lengths
+   res[4] = result.line_endings
+   return res
+end
+
+local function decompress_table(t, fields)
+   local res = {}
+
+   for index, field in ipairs(fields) do
+      local value = t[index]
+
+      if value ~= nil then
+         if field == "options" then
+            value = decompress_table(value, option_fields)
+         end
+
+         res[field] = value
+      end
+   end
+
+   return res
+end
+
+local function decompress_tables(tables, per_code_fields)
+   local res = {}
+
+   for _, t in ipairs(tables) do
+      local fields
+
+      if per_code_fields then
+         fields = stages.warnings[t[1]].fields
+      else
+         fields = parse_inline_options.inline_option_fields
+      end
+
+      table.insert(res, decompress_table(t, fields))
+   end
+
+   return res
+end
+
+local function decompress_result(compressed)
+   local result = {}
+   result.warnings = decompress_tables(compressed[1], true)
+   result.inline_options = decompress_tables(compressed[2])
+   result.line_lengths = compressed[3]
+   result.line_endings = compressed[4]
+   return result
+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, level)
+   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
+      -- Level 1 has the result, level 2 has warning/inline option/line info arrays,
+      -- level 3 has warnings/inline option containers, level 4 has inline options.
+      local allow_sparse = level ~= 3
+      local nil_tail_start
+      local is_sparse
+      local put_one
+      table.insert(buffer, "{")
+
+      for index = 1, max_n(value) do
+         local item = value[index]
+
+         if item == nil then
+            is_sparse = allow_sparse
+            nil_tail_start = nil_tail_start or index
+         else
+            if put_one then
+               table.insert(buffer, ",")
+            end
+
+            if is_sparse then
+               table.insert(buffer, ("[%d]="):format(index))
+            elseif nil_tail_start then
+               for _ = nil_tail_start, index - 1 do
+                  table.insert(buffer, "nil,")
+               end
+
+               nil_tail_start = nil
+            end
+
+            add_value(buffer, strings, item, level + 1)
+            put_one = true
+         end
+      end
+
+      table.insert(buffer, "}")
+   else
+      table.insert(buffer, tostring(value))
+   end
+end
+
+-- Serializes check result, returns a string.
+function serializer.dump_check_result(result)
+   local strings = {}
+   local buffer = {"", "return "}
+   add_value(buffer, strings, compress_result(result), 1)
+
+   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
+
+-- Loads check result from a string, returns check result table or nothing on error.
+function serializer.load_check_result(dumped)
+   local func = utils.load(dumped, {})
+
+   if not func then
+      return
+   end
+
+   local ok, compressed_result = pcall(func)
+
+   if not ok or type(compressed_result) ~= "table" then
+      return
+   end
+
+   return decompress_result(compressed_result)
+end
+
+return serializer
diff --git a/script/luacheck/stages/detect_bad_whitespace.lua b/script/luacheck/stages/detect_bad_whitespace.lua
new file mode 100755
index 000000000..98974d21b
--- /dev/null
+++ b/script/luacheck/stages/detect_bad_whitespace.lua
@@ -0,0 +1,76 @@
+local stage = {}
+
+stage.warnings = {
+   ["611"] = {message_format = "line contains only whitespace", fields = {}},
+   ["612"] = {message_format = "line contains trailing whitespace", fields = {}},
+   ["613"] = {message_format = "trailing whitespace in a string", fields = {}},
+   ["614"] = {message_format = "trailing whitespace in a comment", fields = {}},
+   ["621"] = {message_format = "inconsistent indentation (SPACE followed by TAB)", fields = {}}
+}
+
+function stage.run(chstate)
+   local num_lines = #chstate.line_offsets
+
+   for line_number = 1, num_lines do
+      local line_offset = chstate.line_offsets[line_number]
+      local line_length = chstate.line_lengths[line_number]
+
+      if line_length > 0 then
+         local trailing_ws_pattern
+
+         if line_number == num_lines then
+            trailing_ws_pattern = "^[^\r\n]-()[ \t\f\v]+()[\r\n]?$"
+         else
+            trailing_ws_pattern = "^[^\r\n]-()[ \t\f\v]+()[\r\n]"
+         end
+
+         local line_start_byte, _, trailing_ws_start_byte, line_end_byte = chstate.source:find(
+            trailing_ws_pattern, line_offset)
+
+         local trailing_ws_code
+
+         if trailing_ws_start_byte then
+
+            if trailing_ws_start_byte == line_start_byte then
+               -- Line contains only whitespace (thus never considered "code").
+               trailing_ws_code = "611"
+            elseif not chstate.line_endings[line_number] then
+               -- Trailing whitespace on code line or after long comment.
+               trailing_ws_code = "612"
+            elseif chstate.line_endings[line_number] == "string" then
+               -- Trailing whitespace embedded in a string literal.
+               trailing_ws_code = "613"
+            elseif chstate.line_endings[line_number] == "comment" then
+            -- Trailing whitespace at the end of a line comment or inside long comment.
+               trailing_ws_code = "614"
+            end
+
+            -- The difference between the start and the end of the warning range
+            -- is the same in bytes and in characters because whitespace characters are ASCII.
+            -- Can calculate one based on the three others.
+            local trailing_ws_end_byte = line_end_byte - 1
+            local trailing_ws_end_char = line_offset + line_length - 1
+            local trailing_ws_start_char = trailing_ws_end_char - (trailing_ws_end_byte - trailing_ws_start_byte)
+
+            chstate:warn(trailing_ws_code, line_number, trailing_ws_start_char, trailing_ws_end_char)
+         end
+
+         -- Don't look for inconsistent whitespace in pure whitespace lines.
+         if trailing_ws_code ~= "611" then
+            local leading_ws_start_byte, leading_ws_end_byte = chstate.source:find(
+               "^[ \t\f\v]- \t[ \t\f\v]*", line_offset)
+
+            if leading_ws_start_byte then
+               -- Inconsistent leading whitespace (SPACE followed by TAB).
+
+               -- Calculate warning end in characters using same logic as above.
+               local leading_ws_start_char = line_offset
+               local leading_ws_end_char = leading_ws_start_char + (leading_ws_end_byte - leading_ws_start_byte)
+               chstate:warn("621", line_number, line_offset, leading_ws_end_char)
+            end
+         end
+      end
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_cyclomatic_complexity.lua b/script/luacheck/stages/detect_cyclomatic_complexity.lua
new file mode 100755
index 000000000..22e5988fa
--- /dev/null
+++ b/script/luacheck/stages/detect_cyclomatic_complexity.lua
@@ -0,0 +1,159 @@
+local utils = require "luacheck.utils"
+
+local stage = {}
+
+local function cyclomatic_complexity_message_format(warning)
+   local template = "cyclomatic complexity of %s is too high ({complexity} > {max_complexity})"
+
+   local function_descr
+
+   if warning.function_type == "main_chunk" then
+      function_descr = "main chunk"
+   elseif warning.function_name then
+      function_descr = "{function_type} {function_name!}"
+   else
+      function_descr = "function"
+   end
+
+   return template:format(function_descr)
+end
+
+stage.warnings = {
+   ["561"] = {message_format = cyclomatic_complexity_message_format,
+      fields = {"complexity", "function_type", "function_name"}}
+}
+
+local function warn_cyclomatic_complexity(chstate, line, complexity)
+   if line == chstate.top_line then
+      chstate:warn("561", 1, 1, 1, {
+         complexity = complexity,
+         function_type = "main_chunk"
+      })
+   else
+      local node = line.node
+
+      chstate:warn_range("561", node, {
+         complexity = complexity,
+         function_type = node[1][1] and node[1][1].implicit and "method" or "function",
+         function_name = node.name
+      })
+   end
+end
+
+local CyclomaticComplexityMetric = utils.class()
+
+function CyclomaticComplexityMetric:incr_decisions(count)
+   self.count = self.count + count
+end
+
+function CyclomaticComplexityMetric:calc_expr(node)
+   if node.tag == "Op" and (node[1] == "and" or node[1] == "or") then
+      self:incr_decisions(1)
+   end
+
+   if node.tag ~= "Function" then
+      self:calc_exprs(node)
+   end
+end
+
+function CyclomaticComplexityMetric:calc_exprs(exprs)
+   for _, expr in ipairs(exprs) do
+      if type(expr) == "table" then
+         self:calc_expr(expr)
+      end
+   end
+end
+
+function CyclomaticComplexityMetric:calc_item_Eval(item)
+   self:calc_expr(item.node)
+end
+
+function CyclomaticComplexityMetric:calc_item_Local(item)
+   if item.rhs then
+      self:calc_exprs(item.rhs)
+   end
+end
+
+function CyclomaticComplexityMetric:calc_item_Set(item)
+   self:calc_exprs(item.rhs)
+end
+
+function CyclomaticComplexityMetric:calc_item(item)
+   local f = self["calc_item_" .. item.tag]
+   if f then
+      f(self, item)
+   end
+end
+
+function CyclomaticComplexityMetric:calc_items(items)
+   for _, item in ipairs(items) do
+      self:calc_item(item)
+   end
+end
+
+-- stmt if: {condition, block; condition, block; ... else_block}
+function CyclomaticComplexityMetric:calc_stmt_If(node)
+   for i = 1, #node - 1, 2 do
+      self:incr_decisions(1)
+      self:calc_stmts(node[i+1])
+   end
+
+   if #node % 2 == 1 then
+      self:calc_stmts(node[#node])
+   end
+end
+
+-- stmt while: {condition, block}
+function CyclomaticComplexityMetric:calc_stmt_While(node)
+   self:incr_decisions(1)
+   self:calc_stmts(node[2])
+end
+
+-- stmt repeat: {block, condition}
+function CyclomaticComplexityMetric:calc_stmt_Repeat(node)
+   self:incr_decisions(1)
+   self:calc_stmts(node[1])
+end
+
+-- stmt forin: {iter_vars, expression_list, block}
+function CyclomaticComplexityMetric:calc_stmt_Forin(node)
+   self:incr_decisions(1)
+   self:calc_stmts(node[3])
+end
+
+-- stmt fornum: {first_var, expression, expression, expression[optional], block}
+function CyclomaticComplexityMetric:calc_stmt_Fornum(node)
+   self:incr_decisions(1)
+   self:calc_stmts(node[5] or node[4])
+end
+
+function CyclomaticComplexityMetric:calc_stmt(node)
+   local f = self["calc_stmt_" .. node.tag]
+   if f then
+      f(self, node)
+   end
+end
+
+function CyclomaticComplexityMetric:calc_stmts(stmts)
+   for _, stmt in ipairs(stmts) do
+      self:calc_stmt(stmt)
+   end
+end
+
+-- Cyclomatic complexity of a function equals to the number of decision points plus 1.
+function CyclomaticComplexityMetric:report(chstate, line)
+   self.count = 1
+   self:calc_stmts(line.node[2])
+   self:calc_items(line.items)
+   warn_cyclomatic_complexity(chstate, line, self.count)
+end
+
+function stage.run(chstate)
+   local ccmetric = CyclomaticComplexityMetric()
+
+   for _, line in ipairs(chstate.lines) do
+      ccmetric:report(chstate, line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_empty_blocks.lua b/script/luacheck/stages/detect_empty_blocks.lua
new file mode 100755
index 000000000..b46d89621
--- /dev/null
+++ b/script/luacheck/stages/detect_empty_blocks.lua
@@ -0,0 +1,36 @@
+local core_utils = require "luacheck.core_utils"
+
+local stage = {}
+
+stage.warnings = {
+   ["541"] = {message_format = "empty do..end block", fields = {}},
+   ["542"] = {message_format = "empty if branch", fields = {}}
+}
+
+local function check_block(chstate, block, code)
+   if #block == 0 then
+      chstate:warn_range(code, block)
+   end
+end
+
+
+local function check_node(chstate, node)
+   if node.tag == "Do" then
+      check_block(chstate, node, "541")
+      return
+   end
+
+   for index = 2, #node, 2 do
+      check_block(chstate, node[index], "542")
+   end
+
+   if #node % 2 == 1 then
+      check_block(chstate, node[#node], "542")
+   end
+end
+
+function stage.run(chstate)
+   core_utils.each_statement(chstate, {"Do", "If"}, check_node)
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_empty_statements.lua b/script/luacheck/stages/detect_empty_statements.lua
new file mode 100755
index 000000000..0a9437811
--- /dev/null
+++ b/script/luacheck/stages/detect_empty_statements.lua
@@ -0,0 +1,13 @@
+local stage = {}
+
+stage.warnings = {
+   ["551"] = {message_format = "empty statement", fields = {}}
+}
+
+function stage.run(chstate)
+   for _, range in ipairs(chstate.useless_semicolons) do
+      chstate:warn_range("551", range)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_globals.lua b/script/luacheck/stages/detect_globals.lua
new file mode 100755
index 000000000..59ed41b0d
--- /dev/null
+++ b/script/luacheck/stages/detect_globals.lua
@@ -0,0 +1,252 @@
+local utils = require "luacheck.utils"
+
+local stage = {}
+
+local function prefix_if_indirect(message)
+   return function(warning)
+      if warning.indirect then
+         return "indirectly " .. message
+      else
+         return message
+      end
+   end
+end
+
+local function setting_global_format_message(warning)
+   -- `module` field is set during filtering.
+   if warning.module then
+      return "setting non-module global variable {name!}"
+   else
+      return "setting non-standard global variable {name!}"
+   end
+end
+local global_warning_fields = {"name", "indexing", "previous_indexing_len", "top", "indirect"}
+
+stage.warnings = {
+   ["111"] = {message_format = setting_global_format_message, fields = global_warning_fields},
+   ["112"] = {message_format = "mutating non-standard global variable {name!}", fields = global_warning_fields},
+   ["113"] = {message_format = "accessing undefined variable {name!}", fields = global_warning_fields},
+   -- The following warnings are added during filtering.
+   ["121"] = {message_format = "setting read-only global variable {name!}", fields = {}},
+   ["122"] = {message_format = prefix_if_indirect("setting read-only field {field!} of global {name!}"), fields = {}},
+   ["131"] = {message_format = "unused global variable {name!}", fields = {}},
+   ["142"] = {message_format = prefix_if_indirect("setting undefined field {field!} of global {name!}"), fields = {}},
+   ["143"] = {message_format = prefix_if_indirect("accessing undefined field {field!} of global {name!}"), fields = {}}
+}
+
+local action_codes = {
+   set = "1",
+   mutate = "2",
+   access = "3"
+}
+
+-- `index` describes an indexing, where `index[1]` is a global node
+-- and other items describe keys: each one is a string node, "not_string",
+-- or "unknown". `node` is literal base node that's indexed.
+-- E.g. in `local a = table.a; a.b = "c"` `node` is `a` node of the second
+-- statement and `index` describes `table.a.b`.
+-- `index.previous_indexing_len` is optional length of prefix of `index` array representing last assignment
+-- in the aliasing chain, e.g. `2` in the previous example (because last indexing is `table.a`).
+local function warn_global(chstate, node, index, is_lhs, is_top_line)
+   local global = index[1]
+   local action = is_lhs and (#index == 1 and "set" or "mutate") or "access"
+
+   local indexing
+
+   if #index > 1 then
+      indexing = {}
+
+      for i, field in ipairs(index) do
+         if i > 1 then
+            if field == "unknown" then
+               indexing[i - 1] = true
+            elseif field == "not_string" then
+               indexing[i - 1] = false
+            else
+               indexing[i - 1] = field[1]
+            end
+         end
+      end
+   end
+
+   chstate:warn_range("11" .. action_codes[action], node, {
+      name = global[1],
+      indexing = indexing,
+      previous_indexing_len = index.previous_indexing_len,
+      top = is_top_line and action == "set" or nil,
+      indirect = node ~= global or nil
+   })
+end
+
+local function resolved_to_index(resolution)
+   return resolution ~= "unknown" and resolution ~= "not_string" and resolution.tag ~= "String"
+end
+
+local literal_tags = utils.array_to_set({"Nil", "True", "False", "Number", "String", "Table", "Function"})
+
+local deep_resolve -- Forward declaration.
+
+local function resolve_node(node, item)
+   if node.tag == "Id" or node.tag == "Index" then
+      deep_resolve(node, item)
+      return node.resolution
+   elseif literal_tags[node.tag] then
+      return node.tag == "String" and node or "not_string"
+   else
+      return "unknown"
+   end
+end
+
+-- Resolves value of an identifier or index node, tracking through simple
+-- assignments like `local foo = bar.baz`.
+-- Can be given an `Invoke` node to resolve the method field.
+-- Sets `node.resolution` to "unknown", "not_string", `string node`, or
+-- {previous_indexing_len = index, global_node, key...}.
+-- Each key can be "unknown", "not_string" or `string_node`.
+function deep_resolve(node, item)
+   if node.resolution then
+      return
+   end
+
+   -- Common case.
+   -- Also protects against infinite recursion, if it's even possible.
+   node.resolution = "unknown"
+
+   local base = node
+   local base_tag = node.tag == "Id" and "Id" or "Index"
+   local keys = {}
+
+   while base_tag == "Index" do
+      table.insert(keys, 1, base[2])
+      base = base[1]
+      base_tag = base.tag
+   end
+
+   if base_tag ~= "Id" then
+      return
+   end
+
+   local var = base.var
+   local base_resolution
+   local previous_indexing_len
+
+   if var then
+      if not item.used_values[var] or #item.used_values[var] ~= 1 then
+         -- Do not know where the value for the base local came from.
+         return
+      end
+
+      local value = item.used_values[var][1]
+
+      if not value.node then
+         return
+      end
+
+      base_resolution = resolve_node(value.node, value.item)
+
+      if resolved_to_index(base_resolution) then
+         previous_indexing_len = #base_resolution
+      end
+   else
+      base_resolution = {base}
+   end
+
+   if #keys == 0 then
+      node.resolution = base_resolution
+   elseif not resolved_to_index(base_resolution) then
+      -- Indexing something unknown or indexing a literal.
+      node.resolution = "unknown"
+   else
+      local resolution = utils.update({}, base_resolution)
+      resolution.previous_indexing_len = previous_indexing_len
+
+      for _, key in ipairs(keys) do
+         local key_resolution = resolve_node(key, item)
+
+         if resolved_to_index(key_resolution) then
+            key_resolution = "unknown"
+         end
+
+         table.insert(resolution, key_resolution)
+      end
+
+      -- Assign resolution only after all the recursive calls.
+      node.resolution = resolution
+   end
+end
+
+local function detect_in_node(chstate, item, node, is_top_line, is_lhs)
+   if node.tag == "Index" or node.tag == "Invoke" or node.tag == "Id" then
+      if node.tag == "Id" and node.var then
+         -- Do not warn about assignments to and accesses of local variables
+         -- that resolve to globals or their fields.
+         return
+      end
+
+      deep_resolve(node, item)
+      local resolution = node.resolution
+
+      -- Still need to recurse into base and key nodes.
+      -- E.g. don't miss a global in `(global1())[global2()].
+
+      if node.tag == "Invoke" then
+         for i = 3, #node do
+            detect_in_node(chstate, item, node[i], is_top_line)
+         end
+      end
+
+      if node.tag ~= "Id" then
+         repeat
+            detect_in_node(chstate, item, node[2], is_top_line)
+            node = node[1]
+         until node.tag ~= "Index"
+
+         if node.tag ~= "Id" then
+            detect_in_node(chstate, item, node, is_top_line)
+         end
+      end
+
+      if resolved_to_index(resolution) then
+         warn_global(chstate, node, resolution, is_lhs, is_top_line)
+      end
+   elseif node.tag ~= "Function" then
+      for _, nested_node in ipairs(node) do
+         if type(nested_node) == "table" then
+            detect_in_node(chstate, item, nested_node, is_top_line)
+         end
+      end
+   end
+end
+
+local function detect_in_nodes(chstate, item, nodes, is_top_line, is_lhs)
+   for _, node in ipairs(nodes) do
+      detect_in_node(chstate, item, node, is_top_line, is_lhs)
+   end
+end
+
+local function detect_globals_in_line(chstate, line)
+   local is_top_line = line == chstate.top_line
+
+   for _, item in ipairs(line.items) do
+      if item.tag == "Eval" then
+         detect_in_node(chstate, item, item.node, is_top_line)
+      elseif item.tag == "Local" then
+         if item.rhs then
+            detect_in_nodes(chstate, item, item.rhs, is_top_line)
+         end
+      elseif item.tag == "Set" then
+         detect_in_nodes(chstate, item, item.lhs, is_top_line, true)
+         detect_in_nodes(chstate, item, item.rhs, is_top_line)
+      end
+   end
+end
+
+-- Warns about assignments, field accesses, and mutations of global variables,
+-- tracing through localizing assignments such as `local t = table`.
+function stage.run(chstate)
+   for _, line in ipairs(chstate.lines) do
+      detect_globals_in_line(chstate, line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_reversed_fornum_loops.lua b/script/luacheck/stages/detect_reversed_fornum_loops.lua
new file mode 100755
index 000000000..b09d81930
--- /dev/null
+++ b/script/luacheck/stages/detect_reversed_fornum_loops.lua
@@ -0,0 +1,39 @@
+local core_utils = require "luacheck.core_utils"
+
+local stage = {}
+
+stage.warnings = {
+   ["571"] = {message_format = "numeric for loop goes from #(expr) down to {limit} but loop step is not negative",
+      fields = {"limit"}}
+}
+
+local function check_fornum(chstate, node)
+   if node[2].tag ~= "Op" or node[2][1] ~= "len" then
+      return
+   end
+
+   local limit, limit_repr = core_utils.eval_const_node(node[3])
+
+   if not limit or limit > 1 then
+      return
+   end
+
+   local step = 1
+
+   if node[5] then
+      step = core_utils.eval_const_node(node[4])
+   end
+
+   if step and step >= 0 then
+      chstate:warn_range("571", node, {
+         limit = limit_repr
+      })
+   end
+end
+
+-- Warns about loops trying to go from `#(expr)` to `1` with positive step.
+function stage.run(chstate)
+   core_utils.each_statement(chstate, {"Fornum"}, check_fornum)
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_unbalanced_assignments.lua b/script/luacheck/stages/detect_unbalanced_assignments.lua
new file mode 100755
index 000000000..ab28bf406
--- /dev/null
+++ b/script/luacheck/stages/detect_unbalanced_assignments.lua
@@ -0,0 +1,34 @@
+local core_utils = require "luacheck.core_utils"
+
+local stage = {}
+
+stage.warnings = {
+   ["531"] = {message_format = "right side of assignment has more values than left side expects", fields = {}},
+   ["532"] = {message_format = "right side of assignment has less values than left side expects", fields = {}}
+}
+
+local function is_unpacking(node)
+   return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke"
+end
+
+local function check_assignment(chstate, node)
+   local rhs = node[2]
+
+   if not rhs then
+      return
+   end
+
+   local lhs = node[1]
+
+   if #rhs > #lhs then
+      chstate:warn_range("531", node)
+   elseif #rhs < #lhs and node.tag == "Set" and not is_unpacking(rhs[#rhs]) then
+      chstate:warn_range("532", node)
+   end
+end
+
+function stage.run(chstate)
+   core_utils.each_statement(chstate, {"Set", "Local"}, check_assignment)
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_uninit_accesses.lua b/script/luacheck/stages/detect_uninit_accesses.lua
new file mode 100755
index 000000000..3afe8d2a8
--- /dev/null
+++ b/script/luacheck/stages/detect_uninit_accesses.lua
@@ -0,0 +1,54 @@
+local stage = {}
+
+stage.warnings = {
+   ["321"] = {message_format = "accessing uninitialized variable {name!}", fields = {"name"}},
+   ["341"] = {message_format = "mutating uninitialized variable {name!}", fields = {"name"}}
+}
+
+local function detect_uninit_access_in_line(chstate, line)
+   for _, item in ipairs(line.items) do
+      for _, action_key in ipairs({"accesses", "mutations"}) do
+         local code = action_key == "accesses" and "321" or "341"
+         local item_var_map = item[action_key]
+
+         if item_var_map then
+            for var, accessing_nodes in pairs(item_var_map) do
+               -- If there are no values at all reaching this access, not even the empty one,
+               -- this item (or a closure containing it) is not reachable from variable definition.
+               -- It will be reported as unreachable code, no need to report uninitalized accesses in it.
+               if item.used_values[var] then
+                  -- If this variable is has only one, empty value then it's already reported as never set,
+                  -- no need to report each access.
+                  if not (#var.values == 1 and var.values[1].empty) then
+                     local all_possible_values_empty = true
+
+                     for _, possible_value in ipairs(item.used_values[var]) do
+                        if not possible_value.empty then
+                           all_possible_values_empty = false
+                           break
+                        end
+                     end
+
+                     if all_possible_values_empty then
+                        for _, accessing_node in ipairs(accessing_nodes) do
+                           chstate:warn_range(code, accessing_node, {
+                              name = accessing_node[1]
+                           })
+                        end
+                     end
+                  end
+               end
+            end
+         end
+      end
+   end
+end
+
+-- Warns about accesses and mutations that don't resolve to any values except initial empty one.
+function stage.run(chstate)
+   for _, line in ipairs(chstate.lines) do
+      detect_uninit_access_in_line(chstate, line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_unreachable_code.lua b/script/luacheck/stages/detect_unreachable_code.lua
new file mode 100755
index 000000000..f2003dcff
--- /dev/null
+++ b/script/luacheck/stages/detect_unreachable_code.lua
@@ -0,0 +1,36 @@
+local stage = {}
+
+stage.warnings = {
+   ["511"] = {message_format = "unreachable code", fields = {}},
+   ["512"] = {message_format = "loop is executed at most once", fields = {}}
+}
+
+local function noop_callback() end
+
+local function detect_unreachable_code(chstate, line)
+   local reachable_indexes = {}
+
+   -- Mark all items reachable from the function start.
+   line:walk(reachable_indexes, 1, noop_callback)
+
+   -- All remaining items are unreachable.
+   -- However, there is no point in reporting all of them.
+   -- Only report those that are not reachable from any already reported ones.
+   for item_index, item in ipairs(line.items) do
+      if not reachable_indexes[item_index] then
+         if item.node then
+            chstate:warn_range(item.loop_end and "512" or "511", item.node)
+            -- Mark all items reachable from the item just reported.
+            line:walk(reachable_indexes, item_index, noop_callback)
+         end
+      end
+   end
+end
+
+function stage.run(chstate)
+   for _, line in ipairs(chstate.lines) do
+      detect_unreachable_code(chstate, line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_unused_fields.lua b/script/luacheck/stages/detect_unused_fields.lua
new file mode 100755
index 000000000..2f4cdea6a
--- /dev/null
+++ b/script/luacheck/stages/detect_unused_fields.lua
@@ -0,0 +1,81 @@
+local core_utils = require "luacheck.core_utils"
+
+local stage = {}
+
+local function unused_field_value_message_format(warning)
+   local target = warning.index and "index" or "field"
+   return "value assigned to " .. target .. " {field!} is overwritten on line {overwritten_line} before use"
+end
+
+stage.warnings = {
+   ["314"] = {message_format = unused_field_value_message_format,
+      fields = {"field", "index", "overwritten_line","overwritten_column", "overwritten_end_column"}}
+}
+
+local function warn_unused_field_value(chstate, node, field_repr, is_index, overwriting_node)
+   chstate:warn_range("314", node, {
+      field = field_repr,
+      index = is_index,
+      overwritten_line = overwriting_node.line,
+      overwritten_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.offset),
+      overwritten_end_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.end_offset)
+   })
+end
+
+local function check_table(chstate, node)
+   local array_index = 1.0
+   local key_value_to_node = {}
+   local key_node_to_repr = {}
+   local index_key_nodes = {}
+
+   for _, pair in ipairs(node) do
+      local key_value
+      local key_repr
+      local key_node
+
+      if pair.tag == "Pair" then
+         key_node = pair[1]
+         key_value, key_repr = core_utils.eval_const_node(key_node)
+      else
+         key_node = pair
+         key_value = array_index
+         key_repr = tostring(math.floor(key_value))
+         array_index = array_index + 1.0
+      end
+
+      if key_value then
+         local prev_key_node = key_value_to_node[key_value]
+         local prev_key_repr = key_node_to_repr[prev_key_node]
+         local prev_key_is_index = index_key_nodes[prev_key_node]
+
+         if prev_key_node then
+            warn_unused_field_value(chstate, prev_key_node, prev_key_repr, prev_key_is_index, key_node)
+         end
+
+         key_value_to_node[key_value] = key_node
+         key_node_to_repr[key_node] = key_repr
+
+         if pair.tag ~= "Pair" then
+            index_key_nodes[key_node] = true
+         end
+      end
+   end
+end
+
+local function check_nodes(chstate, nodes)
+   for _, node in ipairs(nodes) do
+      if type(node) == "table" then
+         if node.tag == "Table" then
+            check_table(chstate, node)
+         end
+
+         check_nodes(chstate, node)
+      end
+   end
+end
+
+function stage.run(chstate)
+   check_nodes(chstate, chstate.ast)
+end
+
+return stage
diff --git a/script/luacheck/stages/detect_unused_locals.lua b/script/luacheck/stages/detect_unused_locals.lua
new file mode 100755
index 000000000..47a5b8b95
--- /dev/null
+++ b/script/luacheck/stages/detect_unused_locals.lua
@@ -0,0 +1,330 @@
+local utils = require "luacheck.utils"
+
+local stage = {}
+
+local function unused_local_message_format(warning)
+   if warning.func then
+      if warning.recursive then
+         return "unused recursive function {name!}"
+      elseif warning.mutually_recursive then
+         return "unused mutually recursive function {name!}"
+      else
+         return "unused function {name!}"
+      end
+   else
+      return "unused variable {name!}"
+   end
+end
+
+local function unused_arg_message_format(warning)
+   if warning.name == "..." then
+      return "unused variable length argument"
+   else
+      return "unused argument {name!}"
+   end
+end
+
+local function unused_or_overwritten_warning(message_format)
+   return {
+      message_format = function(warning)
+         if warning.overwritten_line then
+            return message_format .. " is overwritten on line {overwritten_line} before use"
+         else
+            return message_format .. " is unused"
+         end
+      end,
+      fields = {"name", "secondary", "overwritten_line", "overwritten_column", "overwritten_end_column"}
+   }
+end
+
+stage.warnings = {
+   ["211"] = {message_format = unused_local_message_format,
+      fields = {"name", "func", "secondary", "useless", "recursive", "mutually_recursive"}},
+   ["212"] = {message_format = unused_arg_message_format, fields = {"name", "self"}},
+   ["213"] = {message_format = "unused loop variable {name!}", fields = {"name"}},
+   ["221"] = {message_format = "variable {name!} is never set", fields = {"name", "secondary"}},
+   ["231"] = {message_format = "variable {name!} is never accessed", fields = {"name", "secondary"}},
+   ["232"] = {message_format = "argument {name!} is never accessed", fields = {"name"}},
+   ["233"] = {message_format = "loop variable {name!} is never accessed", fields = {"name"}},
+   ["241"] = {message_format = "variable {name!} is mutated but never accessed", fields = {"name", "secondary"}},
+   ["311"] = unused_or_overwritten_warning("value assigned to variable {name!}"),
+   ["312"] = unused_or_overwritten_warning("value of argument {name!}"),
+   ["313"] = unused_or_overwritten_warning("value of loop variable {name!}"),
+   ["331"] = {message_format = "value assigned to variable {name!} is mutated but never accessed",
+      fields = {"name", "secondary"}}
+}
+
+local function is_secondary(value)
+   return value.secondaries and value.secondaries.used
+end
+
+local type_codes = {
+   var = "1",
+   func = "1",
+   arg = "2",
+   loop = "3",
+   loopi = "3"
+}
+
+local function warn_unused_var(chstate, value, is_useless)
+   chstate:warn_value("21" .. type_codes[value.var.type], value, {
+      secondary = is_secondary(value) or nil,
+      func = value.type == "func" or nil,
+      self = value.var.self,
+      useless = value.var.name == "_" and is_useless or nil
+   })
+end
+
+local function warn_unaccessed_var(chstate, var, is_mutated)
+   -- Mark as secondary if all assigned values are secondary.
+   -- It is guaranteed that there are at least two values.
+   local secondary = true
+
+   for _, value in ipairs(var.values) do
+      if not value.empty and not is_secondary(value) then
+         secondary = nil
+         break
+      end
+   end
+
+   chstate:warn_var("2" .. (is_mutated and "4" or "3") .. type_codes[var.type], var, {
+      secondary = secondary
+   })
+end
+
+local function warn_unused_value(chstate, value, overwriting_node)
+   local warning = chstate:warn_value("3" .. (value.mutated and "3" or "1") .. type_codes[value.type], value, {
+      secondary = is_secondary(value) or nil
+   })
+
+   if overwriting_node then
+      warning.overwritten_line = overwriting_node.line
+      warning.overwritten_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.offset)
+      warning.overwritten_end_column = chstate:offset_to_column(overwriting_node.line, overwriting_node.end_offset)
+   end
+end
+
+-- Returns `true` if a variable should be reported as a function instead of simply local,
+-- `false` otherwise.
+-- A variable is considered a function if it has a single assignment and the value is a function,
+-- or if there is a forward declaration with a function assignment later.
+local function is_function_var(var)
+   return (#var.values == 1 and var.values[1].type == "func") or (
+      #var.values == 2 and var.values[1].empty and var.values[2].type == "func")
+end
+
+local externally_accessible_tags = utils.array_to_set({"Id", "Index", "Call", "Invoke", "Op", "Paren", "Dots"})
+
+local function is_externally_accessible(value)
+   return value.type ~= "var" or (value.node and externally_accessible_tags[value.node.tag])
+end
+
+local function get_overwriting_lhs_node(item, value)
+   for _, node in ipairs(item.lhs) do
+      if node.var == value.var then
+         return node
+      end
+   end
+end
+
+local function get_second_overwriting_lhs_node(item, value)
+   local after_value_node
+
+   for _, node in ipairs(item.lhs) do
+      if node.var == value.var then
+         if after_value_node then
+            return node
+         elseif node == value.var_node then
+            after_value_node = true
+         end
+      end
+   end
+end
+
+local function detect_unused_local(chstate, var)
+   if is_function_var(var) then
+      local value = var.values[2] or var.values[1]
+
+      if not value.used then
+         warn_unused_var(chstate, value)
+      end
+   elseif #var.values == 1 then
+      local value = var.values[1]
+
+      if not value.used then
+         if value.mutated then
+            if not is_externally_accessible(value) then
+               warn_unaccessed_var(chstate, var, true)
+            end
+         else
+            warn_unused_var(chstate, value, value.empty)
+         end
+      elseif value.empty then
+         chstate:warn_var("221", var)
+      end
+   elseif not var.accessed and not var.mutated then
+      warn_unaccessed_var(chstate, var)
+   else
+      local no_values_externally_accessible = true
+
+      for _, value in ipairs(var.values) do
+         if is_externally_accessible(value) then
+            no_values_externally_accessible = false
+         end
+      end
+
+      if not var.accessed and no_values_externally_accessible then
+         warn_unaccessed_var(chstate, var, true)
+      end
+
+      for _, value in ipairs(var.values) do
+         if not value.empty then
+            if not value.used then
+               if not value.mutated then
+                  local overwriting_node
+
+                  if value.overwriting_item then
+                     if value.overwriting_item ~= value.item then
+                        overwriting_node = get_overwriting_lhs_node(value.overwriting_item, value)
+                     end
+                  else
+                     overwriting_node = get_second_overwriting_lhs_node(value.item, value)
+                  end
+
+                  warn_unused_value(chstate, value, overwriting_node)
+               elseif not is_externally_accessible(value) then
+                  if var.accessed or not no_values_externally_accessible then
+                     warn_unused_value(chstate, value)
+                  end
+               end
+            end
+         end
+      end
+   end
+end
+
+local function detect_unused_locals_in_line(chstate, line)
+   for _, item in ipairs(line.items) do
+      if item.tag == "Local" then
+         for var in pairs(item.set_variables) do
+            -- Do not check the implicit top level vararg.
+            if var.node.line then
+               detect_unused_local(chstate, var)
+            end
+         end
+      end
+   end
+end
+
+local function detect_unused_locals(chstate)
+   for _, line in ipairs(chstate.lines) do
+      detect_unused_locals_in_line(chstate, line)
+   end
+end
+
+local function mark_reachable_lines(edges, marked, line)
+   for connected_line in pairs(edges[line]) do
+      if not marked[connected_line] then
+         marked[connected_line] = true
+         mark_reachable_lines(edges, marked, connected_line)
+      end
+   end
+end
+
+local function detect_unused_rec_funcs(chstate)
+   -- Build a graph of usage relations of all closures.
+   -- Closure A is used by closure B iff either B is parent
+   -- of A and A is not assigned to a local/upvalue, or
+   -- B uses local/upvalue value that is A.
+   -- Closures not reachable from root closure are unused,
+   -- report corresponding values/variables if not done already.
+
+   local line = chstate.top_line
+
+   -- Initialize edges maps.
+   local forward_edges = {[line] = {}}
+   local backward_edges = {[line] = {}}
+
+   for _, nested_line in ipairs(line.lines) do
+      forward_edges[nested_line] = {}
+      backward_edges[nested_line] = {}
+   end
+
+   -- Add edges leading to each nested line.
+   for _, nested_line in ipairs(line.lines) do
+      if nested_line.node.value then
+         for using_line in pairs(nested_line.node.value.using_lines) do
+            forward_edges[using_line][nested_line] = true
+            backward_edges[nested_line][using_line] = true
+         end
+      elseif nested_line.parent then
+         forward_edges[nested_line.parent][nested_line] = true
+         backward_edges[nested_line][nested_line.parent] = true
+      end
+   end
+
+   -- Recursively mark all closures reachable from root closure and unused closures.
+   -- Closures reachable from main chunk are used; closure reachable from unused closures
+   -- depend on that closure; that is, fixing warning about parent unused closure
+   -- fixes warning about the child one, so issuing a warning for the child is superfluous.
+   local marked = {[line] = true}
+   mark_reachable_lines(forward_edges, marked, line)
+
+   for _, nested_line in ipairs(line.lines) do
+      if nested_line.node.value and not nested_line.node.value.used then
+         marked[nested_line] = true
+         mark_reachable_lines(forward_edges, marked, nested_line)
+      end
+   end
+
+   -- Deal with unused closures.
+   for _, nested_line in ipairs(line.lines) do
+      local value = nested_line.node.value
+
+      if value and value.used and not marked[nested_line] then
+         -- This closure is used by some closure, but is not marked as reachable
+         -- from main chunk or any of reported closures.
+         -- Find candidate group of mutually recursive functions containing this one:
+         -- mark sets of closures reachable from it by forward and backward edges,
+         -- intersect them. Ignore already marked closures in the process to avoid
+         -- issuing superfluous, dependent warnings.
+         local forward_marked = setmetatable({}, {__index = marked})
+         local backward_marked = setmetatable({}, {__index = marked})
+         mark_reachable_lines(forward_edges, forward_marked, nested_line)
+         mark_reachable_lines(backward_edges, backward_marked, nested_line)
+
+         -- Iterate over closures in the group.
+         for mut_rec_line in pairs(forward_marked) do
+            if rawget(backward_marked, mut_rec_line) then
+               marked[mut_rec_line] = true
+               value = mut_rec_line.node.value
+
+               if value then
+                  -- Report this closure as self recursive or mutually recursive.
+                  local is_self_recursive = forward_edges[mut_rec_line][mut_rec_line]
+
+                  if is_function_var(value.var) then
+                     chstate:warn_value("211", value, {
+                        func = true,
+                        mutually_recursive = not is_self_recursive or nil,
+                        recursive = is_self_recursive or nil
+                     })
+                  else
+                     chstate:warn_value("311", value)
+                  end
+               end
+            end
+         end
+      end
+   end
+end
+
+-- Warns about unused local variables and their values as well as locals that
+-- are accessed but never set or set but never accessed.
+-- Warns about unused recursive functions.
+function stage.run(chstate)
+   detect_unused_locals(chstate)
+   detect_unused_rec_funcs(chstate)
+end
+
+return stage
diff --git a/script/luacheck/stages/init.lua b/script/luacheck/stages/init.lua
new file mode 100755
index 000000000..03ff9c796
--- /dev/null
+++ b/script/luacheck/stages/init.lua
@@ -0,0 +1,77 @@
+local utils = require "luacheck.utils"
+
+local stages = {}
+
+-- Checking is organized into stages run one after another.
+-- Each stage is in its own module and provides `run` function operating on a check state,
+-- and optionally `warnings` table mapping issue codes to tables with fields `message_format`
+-- containing format string for the issue or a function returning it given the issue,
+-- and `fields` containing array of extra fields this warning can have.
+
+stages.names = {
+   "parse",
+   "unwrap_parens",
+   "linearize",
+   "parse_inline_options",
+   "name_functions",
+   "resolve_locals",
+   "detect_bad_whitespace",
+   "detect_cyclomatic_complexity",
+   "detect_empty_blocks",
+   "detect_empty_statements",
+   "detect_globals",
+   "detect_reversed_fornum_loops",
+   "detect_unbalanced_assignments",
+   "detect_uninit_accesses",
+   "detect_unreachable_code",
+   "detect_unused_fields",
+   "detect_unused_locals"
+}
+
+local log = require("log")
+stages.modules = {}
+
+for _, name in ipairs(stages.names) do
+   local m,_ = require("luacheck.stages." .. name)
+   table.insert(stages.modules, m)
+end
+
+stages.warnings = {}
+
+local base_fields = {"code", "line", "column", "end_column"}
+
+local function register_warnings(warnings)
+   for code, warning in pairs(warnings) do
+      assert(not stages.warnings[code])
+      assert(warning.message_format)
+      assert(warning.fields)
+
+      local full_fields = utils.concat_arrays({base_fields, warning.fields})
+
+      stages.warnings[code] = {
+         message_format = warning.message_format,
+         fields = full_fields,
+         fields_set = utils.array_to_set(full_fields)
+      }
+   end
+end
+
+-- Issues that do not originate from normal check stages (excluding global related ones).
+register_warnings({
+   ["011"] = {message_format = "{msg}", fields = {"msg", "prev_line", "prev_column", "prev_end_column"}},
+   ["631"] = {message_format = "line is too long ({end_column} > {max_length})", fields = {}}
+})
+
+for _, stage_module in ipairs(stages.modules) do
+   if stage_module.warnings then
+      register_warnings(stage_module.warnings)
+   end
+end
+
+function stages.run(chstate)
+   for _, stage_module in ipairs(stages.modules) do
+      stage_module.run(chstate)
+   end
+end
+
+return stages
diff --git a/script/luacheck/stages/linearize.lua b/script/luacheck/stages/linearize.lua
new file mode 100755
index 000000000..d157227ea
--- /dev/null
+++ b/script/luacheck/stages/linearize.lua
@@ -0,0 +1,708 @@
+local parser = require "luacheck.parser"
+local utils = require "luacheck.utils"
+
+local stage = {}
+
+local function redefined_warning(message_format)
+   return {
+      message_format = message_format,
+      fields = {"name", "prev_line", "prev_column", "prev_end_column", "self"}
+   }
+end
+
+stage.warnings = {
+   ["411"] = redefined_warning("variable {name!} was previously defined on line {prev_line}"),
+   ["412"] = redefined_warning("variable {name!} was previously defined as an argument on line {prev_line}"),
+   ["413"] = redefined_warning("variable {name!} was previously defined as a loop variable on line {prev_line}"),
+   ["421"] = redefined_warning("shadowing definition of variable {name!} on line {prev_line}"),
+   ["422"] = redefined_warning("shadowing definition of argument {name!} on line {prev_line}"),
+   ["423"] = redefined_warning("shadowing definition of loop variable {name!} on line {prev_line}"),
+   ["431"] = redefined_warning("shadowing upvalue {name!} on line {prev_line}"),
+   ["432"] = redefined_warning("shadowing upvalue argument {name!} on line {prev_line}"),
+   ["433"] = redefined_warning("shadowing upvalue loop variable {name!} on line {prev_line}"),
+   ["521"] = {message_format = "unused label {label!}", fields = {"label"}}
+}
+
+local type_codes = {
+   var = "1",
+   func = "1",
+   arg = "2",
+   loop = "3",
+   loopi = "3"
+}
+
+local function warn_redefined(chstate, var, prev_var, is_same_scope)
+   local code = "4" .. (is_same_scope and "1" or var.line == prev_var.line and "2" or "3") .. type_codes[prev_var.type]
+
+   chstate:warn_var(code, var, {
+      self = var.self and prev_var.self,
+      prev_line = prev_var.node.line,
+      prev_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.offset),
+      prev_end_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.end_offset)
+   })
+end
+
+local function warn_unused_label(chstate, label)
+   chstate:warn_range("521", label.range, {
+      label = label.name
+   })
+end
+
+local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"})
+
+local Line = utils.class()
+
+function Line:__init(node, parent, value)
+   -- Maps variables to arrays of accessing items.
+   self.accessed_upvalues = {}
+   -- Maps variables to arrays of mutating items.
+   self.mutated_upvalues = {}
+   -- Maps variables to arays of setting items.
+   self.set_upvalues = {}
+   self.lines = {}
+   self.node = node
+   self.parent = parent
+   self.value = value
+   self.items = utils.Stack()
+end
+
+-- Calls callback with line, index, item, ... for each item reachable from starting item.
+-- `visited` is a set of already visited indexes.
+-- Callback can return true to stop walking from current item.
+function Line:walk(visited, index, callback, ...)
+   if visited[index] then
+      return
+   end
+
+   visited[index] = true
+
+   local item = self.items[index]
+
+   if callback(self, index, item, ...) then
+      return
+   end
+
+   if not item then
+      return
+   elseif item.tag == "Jump" then
+      return self:walk(visited, item.to, callback, ...)
+   elseif item.tag == "Cjump" then
+      self:walk(visited, item.to, callback, ...)
+   end
+
+   return self:walk(visited, index + 1, callback, ...)
+end
+
+local function new_scope(line)
+   return {
+      vars = {},
+      labels = {},
+      gotos = {},
+      line = line
+   }
+end
+
+local function new_var(line, node, type_)
+   return {
+      name = node[1],
+      node = node,
+      type = type_,
+      self = node.implicit,
+      line = line,
+      scope_start = line.items.size + 1,
+      values = {}
+   }
+end
+
+local function new_value(var_node, value_node, item, is_init)
+   local value = {
+      var = var_node.var,
+      var_node = var_node,
+      type = is_init and var_node.var.type or "var",
+      node = value_node,
+      using_lines = {},
+      empty = is_init and not value_node and (var_node.var.type == "var"),
+      item = item
+   }
+
+   if value_node and value_node.tag == "Function" then
+      value.type = "func"
+      value_node.value = value
+   end
+
+   return value
+end
+
+local function new_label(line, name, range)
+   return {
+      name = name,
+      range = range,
+      index = line.items.size + 1
+   }
+end
+
+local function new_goto(name, jump, range)
+   return {
+      name = name,
+      jump = jump,
+      range = range
+   }
+end
+
+local function new_jump_item(is_conditional)
+   return {
+      tag = is_conditional and "Cjump" or "Jump"
+   }
+end
+
+local function new_eval_item(node)
+   return {
+      tag = "Eval",
+      node = node,
+      accesses = {},
+      used_values = {},
+      lines = {}
+   }
+end
+
+local function new_noop_item(node, loop_end)
+   return {
+      tag = "Noop",
+      node = node,
+      loop_end = loop_end
+   }
+end
+
+local function new_local_item(node)
+   return {
+      tag = "Local",
+      node = node,
+      lhs = node[1],
+      rhs = node[2],
+      accesses = node[2] and {},
+      used_values = node[2] and {},
+      lines = node[2] and {}
+   }
+end
+
+local function new_set_item(node)
+   return {
+      tag = "Set",
+      node = node,
+      lhs = node[1],
+      rhs = node[2],
+      accesses = {},
+      mutations = {},
+      used_values = {},
+      lines = {}
+   }
+end
+
+local function is_unpacking(node)
+   return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke"
+end
+
+local LinState = utils.class()
+
+function LinState:__init(chstate)
+   self.chstate = chstate
+   self.lines = utils.Stack()
+   self.scopes = utils.Stack()
+end
+
+function LinState:enter_scope()
+   self.scopes:push(new_scope(self.lines.top))
+end
+
+function LinState:leave_scope()
+   local left_scope = self.scopes:pop()
+   local prev_scope = self.scopes.top
+
+   for _, goto_ in ipairs(left_scope.gotos) do
+      local label = left_scope.labels[goto_.name]
+
+      if label then
+         goto_.jump.to = label.index
+         label.used = true
+      else
+         if not prev_scope or prev_scope.line ~= self.lines.top then
+            if goto_.name == "break" then
+               parser.syntax_error("'break' is not inside a loop", goto_.range)
+            else
+               parser.syntax_error(("no visible label '%s'"):format(goto_.name), goto_.range)
+            end
+         end
+
+         table.insert(prev_scope.gotos, goto_)
+      end
+   end
+
+   for name, label in pairs(left_scope.labels) do
+      if not label.used and not pseudo_labels[name] then
+         warn_unused_label(self.chstate, label)
+      end
+   end
+
+   for _, var in pairs(left_scope.vars) do
+      var.scope_end = self.lines.top.items.size
+   end
+end
+
+function LinState:register_var(node, type_)
+   local var = new_var(self.lines.top, node, type_)
+   local prev_var = self:resolve_var(var.name)
+
+   if prev_var then
+      local is_same_scope = self.scopes.top.vars[var.name]
+
+      if var.name ~= "..." then
+         warn_redefined(self.chstate, var, prev_var, is_same_scope)
+      end
+
+      if is_same_scope then
+         prev_var.scope_end = self.lines.top.items.size
+      end
+   end
+
+   self.scopes.top.vars[var.name] = var
+   node.var = var
+   return var
+end
+
+function LinState:register_vars(nodes, type_)
+   for _, node in ipairs(nodes) do
+      self:register_var(node, type_)
+   end
+end
+
+function LinState:resolve_var(name)
+   for _, scope in utils.ripairs(self.scopes) do
+      local var = scope.vars[name]
+
+      if var then
+         return var
+      end
+   end
+end
+
+function LinState:check_var(node)
+   if not node.var then
+      node.var = self:resolve_var(node[1])
+   end
+
+   return node.var
+end
+
+function LinState:register_label(name, range)
+   local prev_label = self.scopes.top.labels[name]
+
+   if prev_label then
+      assert(not pseudo_labels[name])
+      parser.syntax_error(("label '%s' already defined on line %d"):format(
+         name, prev_label.range.line), range, prev_label.range)
+   end
+
+   self.scopes.top.labels[name] = new_label(self.lines.top, name, range)
+end
+
+function LinState:emit(item)
+   self.lines.top.items:push(item)
+end
+
+function LinState:emit_goto(name, is_conditional, range)
+   local jump = new_jump_item(is_conditional)
+   self:emit(jump)
+   table.insert(self.scopes.top.gotos, new_goto(name, jump, range))
+end
+
+local tag_to_boolean = {
+   Nil = false, False = false,
+   True = true, Number = true, String = true, Table = true, Function = true
+}
+
+-- Emits goto that jumps to ::name:: if bool(cond_node) == false.
+function LinState:emit_cond_goto(name, cond_node)
+   local cond_bool = tag_to_boolean[cond_node.tag]
+
+   if cond_bool ~= true then
+      self:emit_goto(name, cond_bool ~= false)
+   end
+end
+
+function LinState:emit_noop(node, loop_end)
+   self:emit(new_noop_item(node, loop_end))
+end
+
+function LinState:emit_stmt(stmt)
+   self["emit_stmt_" .. stmt.tag](self, stmt)
+end
+
+function LinState:emit_stmts(stmts)
+   for _, stmt in ipairs(stmts) do
+      self:emit_stmt(stmt)
+   end
+end
+
+function LinState:emit_block(block)
+   self:enter_scope()
+   self:emit_stmts(block)
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_Do(node)
+   self:emit_noop(node)
+   self:emit_block(node)
+end
+
+function LinState:emit_stmt_While(node)
+   self:emit_noop(node)
+   self:enter_scope()
+   self:register_label("do")
+   self:emit_expr(node[1])
+   self:emit_cond_goto("break", node[1])
+   self:emit_block(node[2])
+   self:emit_noop(node, true)
+   self:emit_goto("do")
+   self:register_label("break")
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_Repeat(node)
+   self:emit_noop(node)
+   self:enter_scope()
+   self:register_label("do")
+   self:enter_scope()
+   self:emit_stmts(node[1])
+   self:emit_expr(node[2])
+   self:leave_scope()
+   self:emit_cond_goto("do", node[2])
+   self:register_label("break")
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_Fornum(node)
+   self:emit_noop(node)
+   self:emit_expr(node[2])
+   self:emit_expr(node[3])
+
+   if node[5] then
+      self:emit_expr(node[4])
+   end
+
+   self:enter_scope()
+   self:register_label("do")
+   self:emit_goto("break", true)
+   self:enter_scope()
+   self:emit(new_local_item({{node[1]}}))
+   self:register_var(node[1], "loopi")
+   self:emit_stmts(node[5] or node[4])
+   self:leave_scope()
+   self:emit_noop(node, true)
+   self:emit_goto("do")
+   self:register_label("break")
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_Forin(node)
+   self:emit_noop(node)
+   self:emit_exprs(node[2])
+   self:enter_scope()
+   self:register_label("do")
+   self:emit_goto("break", true)
+   self:enter_scope()
+   self:emit(new_local_item({node[1]}))
+   self:register_vars(node[1], "loop")
+   self:emit_stmts(node[3])
+   self:leave_scope()
+   self:emit_noop(node, true)
+   self:emit_goto("do")
+   self:register_label("break")
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_If(node)
+   self:emit_noop(node)
+   self:enter_scope()
+
+   for i = 1, #node - 1, 2 do
+      self:enter_scope()
+      self:emit_expr(node[i])
+      self:emit_cond_goto("else", node[i])
+      self:emit_block(node[i + 1])
+      self:emit_goto("end")
+      self:register_label("else")
+      self:leave_scope()
+   end
+
+   if #node % 2 == 1 then
+      self:emit_block(node[#node])
+   end
+
+   self:register_label("end")
+   self:leave_scope()
+end
+
+function LinState:emit_stmt_Label(node)
+   self:register_label(node[1], node)
+end
+
+function LinState:emit_stmt_Goto(node)
+   self:emit_noop(node)
+   self:emit_goto(node[1], false, node)
+end
+
+function LinState:emit_stmt_Break(node)
+   self:emit_goto("break", false, node)
+end
+
+function LinState:emit_stmt_Return(node)
+   self:emit_noop(node)
+   self:emit_exprs(node)
+   self:emit_goto("return")
+end
+
+function LinState:emit_expr(node)
+   local item = new_eval_item(node)
+   self:scan_expr(item, node)
+   self:emit(item)
+end
+
+function LinState:emit_exprs(exprs)
+   for _, expr in ipairs(exprs) do
+      self:emit_expr(expr)
+   end
+end
+
+LinState.emit_stmt_Call = LinState.emit_expr
+LinState.emit_stmt_Invoke = LinState.emit_expr
+
+function LinState:emit_stmt_Local(node)
+   local item = new_local_item(node)
+   self:emit(item)
+
+   if node[2] then
+      self:scan_exprs(item, node[2])
+   end
+
+   self:register_vars(node[1], "var")
+end
+
+function LinState:emit_stmt_Localrec(node)
+   local item = new_local_item(node)
+   self:register_var(node[1][1], "var")
+   self:emit(item)
+   self:scan_expr(item, node[2][1])
+end
+
+function LinState:emit_stmt_Set(node)
+   local item = new_set_item(node)
+   self:scan_exprs(item, node[2])
+
+   for _, expr in ipairs(node[1]) do
+      if expr.tag == "Id" then
+         local var = self:check_var(expr)
+
+         if var then
+            self:register_upvalue_action(item, var, "set_upvalues")
+         end
+      else
+         assert(expr.tag == "Index")
+         self:scan_lhs_index(item, expr)
+      end
+   end
+
+   self:emit(item)
+end
+
+
+function LinState:scan_expr(item, node)
+   local scanner = self["scan_expr_" .. node.tag]
+
+   if scanner then
+      scanner(self, item, node)
+   end
+end
+
+function LinState:scan_exprs(item, nodes)
+   for _, node in ipairs(nodes) do
+      self:scan_expr(item, node)
+   end
+end
+
+function LinState:register_upvalue_action(item, var, key)
+   for _, line in utils.ripairs(self.lines) do
+      if line == var.line then
+         break
+      end
+
+      if not line[key][var] then
+         line[key][var] = {}
+      end
+
+      table.insert(line[key][var], item)
+   end
+end
+
+function LinState:mark_access(item, node)
+   node.var.accessed = true
+
+   if not item.accesses[node.var] then
+      item.accesses[node.var] = {}
+   end
+
+   table.insert(item.accesses[node.var], node)
+   self:register_upvalue_action(item, node.var, "accessed_upvalues")
+end
+
+function LinState:mark_mutation(item, node)
+   node.var.mutated = true
+
+   if not item.mutations[node.var] then
+      item.mutations[node.var] = {}
+   end
+
+   table.insert(item.mutations[node.var], node)
+   self:register_upvalue_action(item, node.var, "mutated_upvalues")
+end
+
+function LinState:scan_expr_Id(item, node)
+   if self:check_var(node) then
+      self:mark_access(item, node)
+   end
+end
+
+function LinState:scan_expr_Dots(item, node)
+   local dots = self:check_var(node)
+
+   if not dots or dots.line ~= self.lines.top then
+      parser.syntax_error("cannot use '...' outside a vararg function", node)
+   end
+
+   self:mark_access(item, node)
+end
+
+function LinState:scan_lhs_index(item, node)
+   if node[1].tag == "Id" then
+      if self:check_var(node[1]) then
+         self:mark_mutation(item, node[1])
+      end
+   elseif node[1].tag == "Index" then
+      self:scan_lhs_index(item, node[1])
+   else
+      self:scan_expr(item, node[1])
+   end
+
+   self:scan_expr(item, node[2])
+end
+
+LinState.scan_expr_Index = LinState.scan_exprs
+LinState.scan_expr_Call = LinState.scan_exprs
+LinState.scan_expr_Invoke = LinState.scan_exprs
+LinState.scan_expr_Paren = LinState.scan_exprs
+LinState.scan_expr_Table = LinState.scan_exprs
+LinState.scan_expr_Pair = LinState.scan_exprs
+
+function LinState:scan_expr_Op(item, node)
+   self:scan_expr(item, node[2])
+
+   if node[3] then
+      self:scan_expr(item, node[3])
+   end
+end
+
+-- Puts tables {var = value{} into field `set_variables` of items in line which set values.
+-- Registers set values in field `values` of variables.
+function LinState:register_set_variables()
+   local line = self.lines.top
+
+   for _, item in ipairs(line.items) do
+      if item.tag == "Local" or item.tag == "Set" then
+         item.set_variables = {}
+
+         local is_init = item.tag == "Local"
+         local unpacking_item -- Rightmost item of rhs which may unpack into several lhs items.
+
+         if item.rhs then
+            local last_rhs_item = item.rhs[#item.rhs]
+
+            if is_unpacking(last_rhs_item) then
+               unpacking_item = last_rhs_item
+            end
+         end
+
+         local secondaries -- Array of values unpacked from rightmost rhs item.
+
+         if unpacking_item and (#item.lhs > #item.rhs) then
+            secondaries = {}
+         end
+
+         for i, node in ipairs(item.lhs) do
+            local value
+
+            if node.var then
+               value = new_value(node, item.rhs and item.rhs[i] or unpacking_item, item, is_init)
+               item.set_variables[node.var] = value
+               table.insert(node.var.values, value)
+            end
+
+            if secondaries and (i >= #item.rhs) then
+               if value then
+                  value.secondaries = secondaries
+                  table.insert(secondaries, value)
+               else
+                  -- If one of secondary values is assigned to a global or index,
+                  -- it is considered used.
+                  secondaries.used = true
+               end
+            end
+         end
+      end
+   end
+end
+
+function LinState:build_line(node)
+   self.lines:push(Line(node, self.lines.top))
+   self:enter_scope()
+   self:emit(new_local_item({node[1]}))
+   self:enter_scope()
+   self:register_vars(node[1], "arg")
+   self:emit_stmts(node[2])
+   self:leave_scope()
+   self:register_label("return")
+   self:leave_scope()
+   self:register_set_variables()
+   local line = self.lines:pop()
+
+   for _, prev_line in ipairs(self.lines) do
+      table.insert(prev_line.lines, line)
+   end
+
+   return line
+end
+
+function LinState:scan_expr_Function(item, node)
+   local line = self:build_line(node)
+   table.insert(item.lines, line)
+
+   for _, nested_line in ipairs(line.lines) do
+      table.insert(item.lines, nested_line)
+   end
+end
+
+-- Builds linear representation (line) of AST and assigns it as `chstate.top_line`.
+-- Assings an array of all lines as `chstate.lines`.
+-- Adds warnings for redefined/shadowed locals and unused labels.
+function stage.run(chstate)
+   local linstate = LinState(chstate)
+   chstate.top_line = linstate:build_line({{{tag = "Dots", "..."}}, chstate.ast})
+   assert(linstate.lines.size == 0)
+   assert(linstate.scopes.size == 0)
+
+   chstate.lines = {chstate.top_line}
+
+   for _, nested_line in ipairs(chstate.top_line.lines) do
+      table.insert(chstate.lines, nested_line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/name_functions.lua b/script/luacheck/stages/name_functions.lua
new file mode 100755
index 000000000..7ae363ebb
--- /dev/null
+++ b/script/luacheck/stages/name_functions.lua
@@ -0,0 +1,71 @@
+local stage = {}
+
+local function get_index_name(base_name, key_node)
+   if key_node.tag == "String" then
+      return base_name .. "." .. key_node[1]
+   end
+end
+
+local function get_full_field_name(node)
+   if node.tag == "Id" then
+      return node[1]
+   elseif node.tag == "Index" then
+      local base_name = get_full_field_name(node[1])
+      return base_name and get_index_name(base_name, node[2])
+   end
+end
+
+local handle_node
+
+local function handle_nodes(nodes)
+   for _, node in ipairs(nodes) do
+      if type(node) == "table" then
+         handle_node(node)
+      end
+   end
+end
+
+function handle_node(node, name)
+   if node.tag == "Function" then
+      node.name = name
+      handle_nodes(node[2])
+   elseif node.tag == "Set" or node.tag == "Local" or node.tag == "Localrec" then
+      local lhs = node[1]
+      local rhs = node[2]
+
+      -- No need to handle LHS if there is no RHS, it's always just a list of locals in that case.
+      if rhs then
+         handle_nodes(lhs)
+
+         for index, rhs_node in ipairs(rhs) do
+            local lhs_node = lhs[index]
+            local field_name = lhs_node and get_full_field_name(lhs_node)
+            handle_node(rhs_node, field_name)
+         end
+      end
+   elseif node.tag == "Table" and name then
+      for _, pair_node in ipairs(node) do
+         if pair_node.tag == "Pair" then
+            local key_node = pair_node[1]
+            local value_node = pair_node[2]
+            handle_node(key_node)
+            handle_node(value_node, get_index_name(name, key_node))
+         else
+            handle_node(pair_node)
+         end
+      end
+   else
+      handle_nodes(node)
+   end
+end
+
+-- Adds `name` field to `Function` ast nodes when possible:
+-- * Function assigned to a variable (doesn't matter if local or global): "foo".
+-- * Function assigned to a field: "foo.bar.baz".
+--   Function can be in a table assigned to a variable or a field, e.g. `foo.bar = {baz = function() ... end}`.
+-- * Otherwise: `nil`.
+function stage.run(chstate)
+   handle_nodes(chstate.ast)
+end
+
+return stage
diff --git a/script/luacheck/stages/parse.lua b/script/luacheck/stages/parse.lua
new file mode 100755
index 000000000..07e7e650d
--- /dev/null
+++ b/script/luacheck/stages/parse.lua
@@ -0,0 +1,19 @@
+local decoder = require "luacheck.decoder"
+local parser = require "luacheck.parser"
+
+local stage = {}
+
+function stage.run(chstate)
+   chstate.source = decoder.decode(chstate.source_bytes)
+   chstate.line_offsets = {}
+   chstate.line_lengths = {}
+   local ast, comments, code_lines, line_endings, useless_semicolons = parser.parse(
+      chstate.source, chstate.line_offsets, chstate.line_lengths)
+   chstate.ast = ast
+   chstate.comments = comments
+   chstate.code_lines = code_lines
+   chstate.line_endings = line_endings
+   chstate.useless_semicolons = useless_semicolons
+end
+
+return stage
diff --git a/script/luacheck/stages/parse_inline_options.lua b/script/luacheck/stages/parse_inline_options.lua
new file mode 100755
index 000000000..b8f7e5dba
--- /dev/null
+++ b/script/luacheck/stages/parse_inline_options.lua
@@ -0,0 +1,351 @@
+local options = require "luacheck.options"
+local utils = require "luacheck.utils"
+
+local stage = {}
+
+stage.warnings = {
+   -- Also produced during filtering for options that did not pass validation.
+   ["021"] = {message_format = "{msg}", fields = {"msg"}},
+   ["022"] = {message_format = "unpaired push directive", fields = {}},
+   ["023"] = {message_format = "unpaired pop directive", fields = {}}
+}
+
+stage.inline_option_fields = {"line", "pop_count", "options", "column", "end_column"}
+
+local limit_opts = utils.array_to_set({"max_line_length", "max_code_line_length", "max_string_line_length",
+   "max_comment_line_length", "max_cyclomatic_complexity"})
+
+local function is_valid_option_name(name)
+   if name == "std" or options.variadic_inline_options[name] then
+      return true
+   end
+
+   name = name:gsub("^no_", "")
+   return options.nullary_inline_options[name] or limit_opts[name]
+end
+
+-- Splits a token array for an inline option invocation into
+-- option name and argument array, or nil if invocation is invalid.
+local function split_invocation(tokens)
+   -- Name of the option can be split into several space separated tokens.
+   -- Since some valid names are prefixes of some other names
+   -- (e.g. `unused` and `unused arguments`), the longest prefix of token
+   -- array that is a valid option name should be considered.
+   local cur_name
+   local last_valid_name
+   local last_valid_name_end_index
+
+   for i, token in ipairs(tokens) do
+      cur_name = cur_name and (cur_name .. "_" .. token) or token
+
+      if is_valid_option_name(cur_name) then
+         last_valid_name = cur_name
+         last_valid_name_end_index = i
+      end
+   end
+
+   if not last_valid_name then
+      return
+   end
+
+   local args = {}
+
+   for i = last_valid_name_end_index + 1, #tokens do
+      table.insert(args, tokens[i])
+   end
+
+   return last_valid_name, args
+end
+
+local function unexpected_num_args(name, args, expected)
+   return ("inline option '%s' expects %d argument%s, %d given"):format(
+      name, expected, expected == 1 and "" or "s", #args)
+end
+
+-- Parses inline option body, returns options or nil and error message.
+local function parse_options(body)
+   local opts = {}
+
+   local parts = utils.split(body, ",")
+
+   for _, name_and_args in ipairs(parts) do
+      local tokens = utils.split(name_and_args)
+      local name, args = split_invocation(tokens)
+
+      if not name then
+         if #tokens == 0 then
+            return nil, (#parts == 1) and "empty inline option" or "empty inline option invocation"
+         else
+            return nil, ("unknown inline option '%s'"):format(table.concat(tokens, " "))
+         end
+      end
+
+      if name == "std" then
+         if #args ~= 1 then
+            return nil, unexpected_num_args(name, args, 1)
+         end
+
+         opts.std = args[1]
+      elseif name == "ignore" and #args == 0 then
+         opts.ignore = {".*"}
+      elseif options.variadic_inline_options[name] then
+         opts[name] = args
+      else
+         local full_name = name:gsub("_", " ")
+         local subs
+         name, subs = name:gsub("^no_", "")
+         local flag = subs == 0
+
+         if options.nullary_inline_options[name] then
+            if #args ~= 0 then
+               return nil, unexpected_num_args(full_name, args, 0)
+            end
+
+            opts[name] = flag
+         else
+            assert(limit_opts[name])
+
+            if flag then
+               if #args ~= 1 then
+                  return nil, unexpected_num_args(full_name, args, 1)
+               end
+
+               local value = tonumber(args[1])
+
+               if not value then
+                  return nil, ("inline option '%s' expects number as argument"):format(name)
+               end
+
+               opts[name] = value
+            else
+               if #args ~= 0 then
+                  return nil, unexpected_num_args(full_name, args, 0)
+               end
+
+               opts[name] = false
+            end
+         end
+      end
+   end
+
+   return opts
+end
+
+-- Parses comment contents, returns up to two `options` values (tables or "push" or "pop").
+-- On an invalid inline comment returns nil and an error message.
+local function parse_inline_comment(comment_contents)
+   local body = utils.after(utils.strip(comment_contents), "^luacheck:")
+
+   if not body then
+      return
+   end
+
+   local opts1, opts2
+
+   -- Remove comments in balanced parens.
+   body = utils.strip((body:gsub("%b()", " ")))
+   local after_push = body:match("^push%s+(.*)")
+
+   if after_push then
+      opts2 = "push"
+      body = after_push
+   elseif body == "push" or body == "pop" then
+      return body
+   end
+
+   local err_msg
+   opts1, err_msg = parse_options(body)
+   return opts1, err_msg or opts2
+end
+
+-- Returns an array of tables with column range info and an `options` field
+-- containing a table of options or "push" or "pop".
+-- Warns about invalid inline option comments.
+local function parse_inline_comments(chstate)
+   local res = {}
+
+   for _, comment in ipairs(chstate.comments) do
+      local opts1, opts2 = parse_inline_comment(comment.contents)
+
+      if opts1 then
+         table.insert(res, {
+            line = comment.line,
+            column = chstate:offset_to_column(comment.line, comment.offset),
+            end_column = chstate:offset_to_column(comment.line, comment.end_offset),
+            options = opts1
+         })
+
+         if opts2 then
+            table.insert(res, {
+               line = comment.line,
+               column = chstate:offset_to_column(comment.line, comment.offset),
+               end_column = chstate:offset_to_column(comment.line, comment.end_offset),
+               options = opts2
+            })
+         end
+      elseif opts2 then
+         chstate:warn_range("021", comment, {msg = opts2})
+      end
+   end
+
+   return res
+end
+
+-- Adds a table with `line`, `column`, and `options` fields to given array.
+-- For each function a table with `options` set to "push" for the function start
+-- and a talbe with `options` set to "pop" for the function end are added.
+local function add_function_boundaries(inline_options_and_boundaries, chstate)
+   for _, line in ipairs(chstate.top_line.lines) do
+      local fn_node = line.node
+
+      table.insert(inline_options_and_boundaries, {
+         line = fn_node.line,
+         column = chstate:offset_to_column(fn_node.line, fn_node.offset),
+         options = "push"
+      })
+
+      table.insert(inline_options_and_boundaries, {
+         line = fn_node.end_range.line,
+         column = chstate:offset_to_column(fn_node.end_range.line, fn_node.end_range.offset),
+         options = "pop"
+      })
+   end
+end
+
+local function get_order(t)
+   if t.options == "push" then
+      return 1
+   elseif t.options == "pop" then
+      return 3
+   else
+      return 2
+   end
+end
+
+local function options_and_boundaries_comparator(t1, t2)
+   if t1.line ~= t2.line then
+      return t1.line < t2.line
+   end
+
+   -- For options and boundaries on the same line, all pushes are applied before options before pops.
+   -- (Valid pops will be moved to the start of the next line later.)
+   local order1 = get_order(t1)
+   local order2 = get_order(t2)
+
+   if order1 ~= order2 then
+      return order1 < order2
+   else
+      return t1.column < t2.column
+   end
+end
+
+-- Applies bounadaries withing `inline_options_and_boundaries` to replace them with pop count
+-- instructions in the resulting array.
+-- Comments on lines with code are popped at the end of line.
+-- Warns about unpaired push and pop directives.
+local function apply_boundaries(chstate, inline_options_and_boundaries)
+   local res = {}
+   local res_last
+
+   -- While iterating over inline options and boundaries track push
+   -- boundaries that were not popped yet plus the number of options
+   -- that would be on the option stack after applying all already
+   -- processed option table pushes and pops.
+   local pushes = utils.Stack()
+   local push_option_counts = utils.Stack()
+   local option_count = 0
+
+   for _, item in ipairs(inline_options_and_boundaries) do
+      if item.options == "push" then
+         pushes:push(item)
+         push_option_counts:push(option_count)
+      elseif item.options == "pop" then
+         -- Function boundaries are implicit, don't allow inline options to pop
+         -- them, don't allow function boundaries to pop inline option pushes either.
+         -- Inline options boundaries have end_column, function boundaries don't.
+         if not pushes.top or (item.end_column and not pushes.top.end_column) then
+            -- Inline option pop against nothing or a function push, mark as unpaired.
+            chstate:warn_column_range("023", item)
+         else
+            if not item.end_column then
+               -- Function pop, remove any unpaired inline option pushes.
+               while pushes.top and pushes.top.end_column do
+                  chstate:warn_column_range("022", pushes.top)
+                  pushes:pop()
+                  push_option_counts:pop()
+               end
+            end
+
+            pushes:pop()
+            local prev_option_count = push_option_counts:pop()
+            local pop_count = option_count - prev_option_count
+
+            if pop_count > 0 then
+               -- Place the pop instruction at the start of the next line so that getting option stack
+               -- for a line amounts to applying both the pop instruction and the option push for the line.
+               local line = item.line + 1
+
+               -- Collapse with a previous table if it's on the same line. It can only be a pop count table.
+               if res_last and res_last.line == line then
+                  res_last.pop_count = res_last.pop_count + pop_count
+               else
+                  res_last = {
+                     line = line,
+                     pop_count = pop_count
+                  }
+
+                  table.insert(res, res_last)
+               end
+            end
+
+            -- Update option stack size for this pop.
+            option_count = prev_option_count
+         end
+      else
+         -- Inline options table. Check if there is a pop count table for this line already.
+         if res_last and res_last.line == item.line then
+            res_last.options = item.options
+            res_last.column = item.column
+            res_last.end_column = item.end_column
+         else
+            res_last = item
+            table.insert(res, item)
+         end
+
+         if chstate.code_lines[item.line] then
+            -- Inline comment on a line with some code, immediately pop it.
+            res_last = {
+               line = item.line + 1,
+               pop_count = 1
+            }
+            table.insert(res, res_last)
+         else
+            option_count = option_count + 1
+         end
+      end
+   end
+
+   -- Any remaining pushes are unpaired inline comments from the main chunk.
+   while pushes.top do
+      chstate:warn_column_range("022", pushes:pop())
+   end
+
+   return res
+end
+
+-- Warns about invalid inline options.
+-- Sets `chstate.inline_options` to an array of tables that describe the way inline option tables
+-- are pushed onto and popped from the option stack when iterating over lines.
+-- Each table has field `line` that the array is sorted by and also ether or both sets of fields:
+-- * `pop_count` - refers to a number of option tables that should be popped from the stack before processing
+--   warnings on this line.
+-- * `options`, `column`, `end_column` - refers to an option table that should be pushed onto the stack
+--   before processing warnings on this line but after popping tables if `pop_count` is present.
+function stage.run(chstate)
+   local inline_options_and_boundaries = parse_inline_comments(chstate)
+   add_function_boundaries(inline_options_and_boundaries, chstate)
+   table.sort(inline_options_and_boundaries, options_and_boundaries_comparator)
+   chstate.inline_options = apply_boundaries(chstate, inline_options_and_boundaries)
+end
+
+return stage
diff --git a/script/luacheck/stages/resolve_locals.lua b/script/luacheck/stages/resolve_locals.lua
new file mode 100755
index 000000000..c045609b0
--- /dev/null
+++ b/script/luacheck/stages/resolve_locals.lua
@@ -0,0 +1,216 @@
+local stage = {}
+
+-- The main part of analysis is connecting assignments to locals or upvalues
+-- with accesses that may use the assigned value.
+-- Accesses and assignments are split into two groups based on whether they happen
+-- in the closure that defines subject local variable (main assignment, main access)
+-- or in some nested closure (closure assignment, closure access).
+-- To avoid false positives, it's assumed that a closure may be called at any point
+-- starting from expression that creates it.
+-- Additionally, all operations on upvalues are considered in bulk, as in,
+-- when a closure is called, it's assumed that any subset of its upvalue assignments
+-- and accesses may happen, in any order.
+
+-- Assignments and accesses are connected based on whether they can reach each other.
+-- A main assignment is connected with a main access when the assignment can reach the access.
+-- A main assignment is connected with a closure access when the assignment can reach the closure creation
+-- or the closure creation can reach the assignment.
+-- A closure assignment is connected with a main access when the closure creation can reach the access.
+-- A closure assignment is connected with a closure access when either closure creation can reach the other one.
+
+-- To determine what flow graph nodes an assignment or a closure creation can reach,
+-- they are independently propagated along the graph.
+-- Closure creation propagation is not bounded.
+-- Main assignment propagation is bounded by entrance and exit conditions for each reached flow graph node.
+-- Entrance condition checks that target local variable is still in scope. If entrance condition fails,
+-- nothing in the node can refer to the variable, and the scope can't be reentered later.
+-- So, in this case, assignment does not reach the node, and propagation does not continue.
+-- Exit condition checks that target local variable is not overwritten by an assignment in the node.
+-- If it fails, the assignment still reaches the node (because all accesses in a node are evaluated before any
+-- assignments take effect), but propagation does not continue.
+
+local function register_value(values_per_var, var, value)
+   if not values_per_var[var] then
+      values_per_var[var] = {}
+   end
+
+   table.insert(values_per_var[var], value)
+end
+
+-- Called when assignment of `value` is connected to an access.
+-- `item` contains the access, and `line` contains the item.
+local function add_resolution(line, item, var, value, is_mutation)
+   register_value(item.used_values, var, value)
+   value[is_mutation and "mutated" or "used"] = true
+   value.using_lines[line] = true
+
+   if value.secondaries then
+      value.secondaries.used = true
+   end
+end
+
+-- Connects accesses in given items array with an assignment of `value`.
+-- `items` may be `nil` instead of empty.
+local function add_resolutions(line, items, var, value, is_mutation)
+   if not items then
+      return
+   end
+
+   for _, item in ipairs(items) do
+      add_resolution(line, item, var, value, is_mutation)
+   end
+end
+
+-- Connects all accesses (and mutations) in `access_line` with corresponding
+-- assignments in `set_line`.
+local function cross_resolve_closures(access_line, set_line)
+   for var, setting_items in pairs(set_line.set_upvalues) do
+      for _, setting_item in ipairs(setting_items) do
+         add_resolutions(access_line, access_line.accessed_upvalues[var],
+            var, setting_item.set_variables[var])
+         add_resolutions(access_line, access_line.mutated_upvalues[var],
+            var, setting_item.set_variables[var], true)
+      end
+   end
+end
+
+local function in_scope(var, index)
+   return (var.scope_start <= index) and (index <= var.scope_end)
+end
+
+-- Called when main assignment propagation reaches a line item.
+local function main_assignment_propagation_callback(line, index, item, var, value)
+   -- Check entrance condition.
+   if not in_scope(var, index) then
+      -- Assignment reaches the end of variable scope, so it can't be dominated by any assignment.
+      value.overwriting_item = false
+      return true
+   end
+
+   -- Assignment reaches this item, apply its effect.
+
+   -- Accesses (and mutations) of the variable can resolve to reaching assignment.
+   if item.accesses and item.accesses[var] then
+      add_resolution(line, item, var, value)
+   end
+
+   if item.mutations and item.mutations[var] then
+      add_resolution(line, item, var, value, true)
+   end
+
+   -- Accesses (and mutations) of the variable inside closures created in this item
+   -- can resolve to reaching assignment.
+   if item.lines then
+      for _, created_line in ipairs(item.lines) do
+         add_resolutions(created_line, created_line.accessed_upvalues[var], var, value)
+         add_resolutions(created_line, created_line.mutated_upvalues[var], var, value, true)
+      end
+   end
+
+   -- Check exit condition.
+   if item.set_variables and item.set_variables[var] then
+      if value.overwriting_item ~= false then
+         if value.overwriting_item and value.overwriting_item ~= item then
+            value.overwriting_item = false
+         else
+            value.overwriting_item = item
+         end
+      end
+
+      return true
+   end
+end
+
+-- Connects main assignments with main accesses and closure accesses in reachable closures.
+-- Additionally, sets `overwriting_item` field of values to an item with an assignment overwriting
+-- the value, but only if the overwriting is not avoidable (i.e. it's impossible to reach end of function
+-- from the first assignment without going through the second one). Otherwise value of the field may be
+-- `false` or `nil`.
+local function propagate_main_assignments(line)
+   for i, item in ipairs(line.items) do
+      if item.set_variables then
+         for var, value in pairs(item.set_variables) do
+            if var.line == line then
+               -- Assignments are not live at their own item, because assignments take effect only after all accesses
+               -- are evaluated. Items with assignments can't be jumps, so they have a single following item
+               -- with incremented index.
+               line:walk({}, i + 1, main_assignment_propagation_callback, var, value)
+            end
+         end
+      end
+   end
+end
+
+-- Called when closure creation propagation reaches a line item.
+local function closure_creation_propagation_callback(line, _, item, propagated_line)
+   if not item then
+      return true
+   end
+
+   -- Closure creation reaches this item, apply its effects.
+
+   -- Accesses (and mutations) of upvalues in the propagated closure
+   -- can resolve to assignments in the item.
+   if item.set_variables then
+      for var, value in pairs(item.set_variables) do
+         add_resolutions(propagated_line, propagated_line.accessed_upvalues[var], var, value)
+         add_resolutions(propagated_line, propagated_line.mutated_upvalues[var], var, value, true)
+      end
+   end
+
+   if item.lines then
+      for _, created_line in ipairs(item.lines) do
+         -- Accesses (and mutations) of upvalues in the propagated closure
+         -- can resolve to assignments in closures created in the item.
+         cross_resolve_closures(propagated_line, created_line)
+
+         -- Accesses (and mutations) of upvalues in closures created in the item
+         -- can resolve to assignments in the propagated closure.
+         cross_resolve_closures(created_line, propagated_line)
+      end
+   end
+
+   -- Accesses (and mutations) of locals in the item can resolve
+   -- to assignments in the propagated closure.
+   for var, setting_items in pairs(propagated_line.set_upvalues) do
+      if item.accesses and item.accesses[var] then
+         for _, setting_item in ipairs(setting_items) do
+            add_resolution(line, item, var, setting_item.set_variables[var])
+         end
+      end
+
+      if item.mutations and item.mutations[var] then
+         for _, setting_item in ipairs(setting_items) do
+            add_resolution(line, item, var, setting_item.set_variables[var], true)
+         end
+      end
+   end
+end
+
+-- Connects main assignments with closure accesses in reaching closures.
+-- Connects closure assignments with main accesses and with closure accesses in reachable closures.
+-- Connects closure accesses with closure assignments in reachable closures.
+local function propagate_closure_creations(line)
+   for i, item in ipairs(line.items) do
+      if item.lines then
+         for _, created_line in ipairs(item.lines) do
+            -- Closures are live at the item they are created, as they can be called immediately.
+            line:walk({}, i, closure_creation_propagation_callback, created_line)
+         end
+      end
+   end
+end
+
+local function analyze_line(line)
+   propagate_main_assignments(line)
+   propagate_closure_creations(line)
+end
+
+-- Finds reaching assignments for all local variable accesses.
+function stage.run(chstate)
+   for _, line in ipairs(chstate.lines) do
+      analyze_line(line)
+   end
+end
+
+return stage
diff --git a/script/luacheck/stages/unwrap_parens.lua b/script/luacheck/stages/unwrap_parens.lua
new file mode 100755
index 000000000..c4c860bf8
--- /dev/null
+++ b/script/luacheck/stages/unwrap_parens.lua
@@ -0,0 +1,52 @@
+local stage = {}
+
+-- Mutates an array of nodes and non-tables, unwrapping Paren nodes.
+-- If list_start is given, tail Paren is not unwrapped if it's unpacking and past list_start index.
+local function handle_nodes(nodes, list_start)
+   local num_nodes = #nodes
+
+   for index = 1, num_nodes do
+      local node = nodes[index]
+
+      if type(node) == "table" then
+         local tag = node.tag
+
+         if tag == "Table" or tag == "Return" then
+            handle_nodes(node, 1)
+         elseif tag == "Call" then
+            handle_nodes(node, 2)
+         elseif tag == "Invoke" then
+            handle_nodes(node, 3)
+         elseif tag == "Forin" then
+            handle_nodes(node[2], 1)
+            handle_nodes(node[3])
+         elseif tag == "Local" then
+            if node[2] then
+               handle_nodes(node[2])
+            end
+         elseif tag == "Set" then
+            handle_nodes(node[1])
+            handle_nodes(node[2], 1)
+         else
+            handle_nodes(node)
+
+            if tag == "Paren" and (not list_start or index < list_start or index ~= num_nodes) then
+               local inner_node = node[1]
+
+               if inner_node.tag ~= "Call" and inner_node.tag ~= "Invoke" and inner_node.tag ~= "Dots" then
+                  nodes[index] = inner_node
+               end
+            end
+         end
+      end
+   end
+end
+
+-- Mutates AST, unwrapping Paren nodes.
+-- Paren nodes are preserved only when they matter:
+-- at the ends of expression lists with potentially multi-value inner expressions.
+function stage.run(chstate)
+   handle_nodes(chstate.ast)
+end
+
+return stage
diff --git a/script/luacheck/standards.lua b/script/luacheck/standards.lua
new file mode 100755
index 000000000..f3d8bd654
--- /dev/null
+++ b/script/luacheck/standards.lua
@@ -0,0 +1,269 @@
+local 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.
+-- `index` is an optional string specifying position of the field table in the root table.
+-- Returns a true if the table is valid, false, an error message, and index of the table with the error otherwise.
+local function validate_fields(fields, is_root, index)
+   if fields == nil then
+      return true
+   end
+
+   local field_type = is_root and "global" or "field"
+
+   if type(fields) ~= "table" then
+      return false, ("%ss table expected, got %s"):format(field_type, type(fields)), index
+   end
+
+   for key, value in pairs(fields) do
+      if type(key) == "string" then
+         local new_index = (index or "") .. "." .. key
+
+         if type(value) ~= "table" then
+            return false, ("%s description table expected, got %s"):format(field_type, type(value)), new_index
+         end
+
+         if value.read_only ~= nil and type(value.read_only) ~= "boolean" then
+            local err = "invalid value of option 'read_only': boolean expected, got " .. type(value.read_only)
+            return false, err, new_index
+         end
+
+         if value.other_fields ~= nil and type(value.other_fields) ~= "boolean" then
+            local err = "invalid value of option 'other_fields': boolean expected, got " .. type(value.other_fields)
+            return false, err, new_index
+         end
+
+         local ok, err, err_index = validate_fields(value.fields, false, new_index .. ".fields")
+
+         if not ok then
+            return false, err, err_index
+         end
+      elseif type(value) ~= "string" then
+         local key_as_string = type(key) == "number" and ("%.20g"):format(key) or ("<%s>"):format(type(key))
+         local new_index = ("%s[%s]"):format(index or "", key_as_string)
+         return false, ("string expected as %s name, got %s"):format(field_type, type(value)), new_index
+      end
+   end
+
+   return true
+end
+
+-- Validates a field table.
+-- Returns true if the table is valid, false and an error message otherwise.
+function standards.validate_globals_table(globals_table)
+   local ok, err, err_index = validate_fields(globals_table, true)
+
+   if ok then
+      return true
+   end
+
+   local err_prefix = err_index and ("in field %s: "):format(err_index) or ""
+   return false, err_prefix .. err
+end
+
+-- Validates an std table in user-side format.
+-- Returns true if the table is valid, false and an error message otherwise.
+function standards.validate_std_table(std_table)
+   local ok, err, err_index = validate_fields(std_table.globals, true, ".globals")
+
+   if ok then
+      ok, err, err_index = validate_fields(std_table.read_globals, true, ".read_globals")
+   end
+
+   if ok then
+      return true
+   end
+
+   local err_prefix = ("in field %s: "):format(err_index)
+   return false, err_prefix .. err
+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
diff --git a/script/luacheck/unicode.lua b/script/luacheck/unicode.lua
new file mode 100755
index 000000000..e861aece4
--- /dev/null
+++ b/script/luacheck/unicode.lua
@@ -0,0 +1,36 @@
+local unicode_printability_boundaries = require "luacheck.unicode_printability_boundaries"
+
+local unicode = {}
+
+-- unicode_printability_boundaries is an array of first codepoints of
+-- each continuous block of codepoints that are all printable or all not printable.
+
+function unicode.is_printable(codepoint)
+   -- Binary search for index of the first boundary less than or equal to given codepoint.
+   local floor_boundary_index
+
+   -- Target index is always in [begin_index..end_index).
+   local begin_index = 1
+   local end_index = #unicode_printability_boundaries + 1
+
+   while end_index - begin_index > 1 do
+      local mid_index = math.floor((begin_index + end_index) / 2)
+      local mid_codepoint = unicode_printability_boundaries[mid_index]
+
+      if codepoint < mid_codepoint then
+         end_index = mid_index
+      elseif codepoint > mid_codepoint then
+         begin_index = mid_index
+      else
+         floor_boundary_index = mid_index
+         break
+      end
+   end
+
+   floor_boundary_index = floor_boundary_index or begin_index
+   -- floor_boundary_index is the number of the block containing codepoint.
+   -- Printable and not printable blocks alternate and the first one is not printable (zero is not printable).
+   return floor_boundary_index % 2 == 0
+end
+
+return unicode
diff --git a/script/luacheck/unicode_printability_boundaries.lua b/script/luacheck/unicode_printability_boundaries.lua
new file mode 100755
index 000000000..d597a304b
--- /dev/null
+++ b/script/luacheck/unicode_printability_boundaries.lua
@@ -0,0 +1,2 @@
+-- Autogenerated using data from https://www.unicode.org/Public/11.0.0/ucd/UnicodeData.txt
+return {0,32,127,160,173,174,888,890,896,900,907,908,909,910,930,931,1328,1329,1367,1369,1419,1421,1424,1425,1480,1488,1515,1519,1525,1542,1564,1566,1757,1758,1806,1808,1867,1869,1970,1984,2043,2045,2094,2096,2111,2112,2140,2142,2143,2144,2155,2208,2229,2230,2238,2259,2274,2275,2436,2437,2445,2447,2449,2451,2473,2474,2481,2482,2483,2486,2490,2492,2501,2503,2505,2507,2511,2519,2520,2524,2526,2527,2532,2534,2559,2561,2564,2565,2571,2575,2577,2579,2601,2602,2609,2610,2612,2613,2615,2616,2618,2620,2621,2622,2627,2631,2633,2635,2638,2641,2642,2649,2653,2654,2655,2662,2679,2689,2692,2693,2702,2703,2706,2707,2729,2730,2737,2738,2740,2741,2746,2748,2758,2759,2762,2763,2766,2768,2769,2784,2788,2790,2802,2809,2816,2817,2820,2821,2829,2831,2833,2835,2857,2858,2865,2866,2868,2869,2874,2876,2885,2887,2889,2891,2894,2902,2904,2908,2910,2911,2916,2918,2936,2946,2948,2949,2955,2958,2961,2962,2966,2969,2971,2972,2973,2974,2976,2979,2981,2984,2987,2990,3002,3006,3011,3014,3017,3018,3022,3024,3025,3031,3032,3046,3067,3072,3085,3086,3089,3090,3113,3114,3130,3133,3141,3142,3145,3146,3150,3157,3159,3160,3163,3168,3172,3174,3184,3192,3213,3214,3217,3218,3241,3242,3252,3253,3258,3260,3269,3270,3273,3274,3278,3285,3287,3294,3295,3296,3300,3302,3312,3313,3315,3328,3332,3333,3341,3342,3345,3346,3397,3398,3401,3402,3408,3412,3428,3430,3456,3458,3460,3461,3479,3482,3506,3507,3516,3517,3518,3520,3527,3530,3531,3535,3541,3542,3543,3544,3552,3558,3568,3570,3573,3585,3643,3647,3676,3713,3715,3716,3717,3719,3721,3722,3723,3725,3726,3732,3736,3737,3744,3745,3748,3749,3750,3751,3752,3754,3756,3757,3770,3771,3774,3776,3781,3782,3783,3784,3790,3792,3802,3804,3808,3840,3912,3913,3949,3953,3992,3993,4029,4030,4045,4046,4059,4096,4294,4295,4296,4301,4302,4304,4681,4682,4686,4688,4695,4696,4697,4698,4702,4704,4745,4746,4750,4752,4785,4786,4790,4792,4799,4800,4801,4802,4806,4808,4823,4824,4881,4882,4886,4888,4955,4957,4989,4992,5018,5024,5110,5112,5118,5120,5789,5792,5881,5888,5901,5902,5909,5920,5943,5952,5972,5984,5997,5998,6001,6002,6004,6016,6110,6112,6122,6128,6138,6144,6158,6160,6170,6176,6265,6272,6315,6320,6390,6400,6431,6432,6444,6448,6460,6464,6465,6468,6510,6512,6517,6528,6572,6576,6602,6608,6619,6622,6684,6686,6751,6752,6781,6783,6794,6800,6810,6816,6830,6832,6847,6912,6988,6992,7037,7040,7156,7164,7224,7227,7242,7245,7305,7312,7355,7357,7368,7376,7418,7424,7674,7675,7958,7960,7966,7968,8006,8008,8014,8016,8024,8025,8026,8027,8028,8029,8030,8031,8062,8064,8117,8118,8133,8134,8148,8150,8156,8157,8176,8178,8181,8182,8191,8192,8203,8208,8232,8239,8288,8304,8306,8308,8335,8336,8349,8352,8384,8400,8433,8448,8588,8592,9255,9280,9291,9312,11124,11126,11158,11160,11209,11210,11263,11264,11311,11312,11359,11360,11508,11513,11558,11559,11560,11565,11566,11568,11624,11631,11633,11647,11671,11680,11687,11688,11695,11696,11703,11704,11711,11712,11719,11720,11727,11728,11735,11736,11743,11744,11855,11904,11930,11931,12020,12032,12246,12272,12284,12288,12352,12353,12439,12441,12544,12549,12592,12593,12687,12688,12731,12736,12772,12784,12831,12832,13055,13056,19894,19904,40944,40960,42125,42128,42183,42192,42540,42560,42744,42752,42938,42999,43052,43056,43066,43072,43128,43136,43206,43214,43226,43232,43348,43359,43389,43392,43470,43471,43482,43486,43519,43520,43575,43584,43598,43600,43610,43612,43715,43739,43767,43777,43783,43785,43791,43793,43799,43808,43815,43816,43823,43824,43878,43888,44014,44016,44026,44032,55204,55216,55239,55243,55292,63744,64110,64112,64218,64256,64263,64275,64280,64285,64311,64312,64317,64318,64319,64320,64322,64323,64325,64326,64450,64467,64832,64848,64912,64914,64968,65008,65022,65024,65050,65056,65107,65108,65127,65128,65132,65136,65141,65142,65277,65281,65471,65474,65480,65482,65488,65490,65496,65498,65501,65504,65511,65512,65519,65532,65534,65536,65548,65549,65575,65576,65595,65596,65598,65599,65614,65616,65630,65664,65787,65792,65795,65799,65844,65847,65935,65936,65948,65952,65953,66000,66046,66176,66205,66208,66257,66272,66300,66304,66340,66349,66379,66384,66427,66432,66462,66463,66500,66504,66518,66560,66718,66720,66730,66736,66772,66776,66812,66816,66856,66864,66916,66927,66928,67072,67383,67392,67414,67424,67432,67584,67590,67592,67593,67594,67638,67639,67641,67644,67645,67647,67670,67671,67743,67751,67760,67808,67827,67828,67830,67835,67868,67871,67898,67903,67904,67968,68024,68028,68048,68050,68100,68101,68103,68108,68116,68117,68120,68121,68150,68152,68155,68159,68169,68176,68185,68192,68256,68288,68327,68331,68343,68352,68406,68409,68438,68440,68467,68472,68498,68505,68509,68521,68528,68608,68681,68736,68787,68800,68851,68858,68904,68912,68922,69216,69247,69376,69416,69424,69466,69632,69710,69714,69744,69759,69821,69822,69826,69840,69865,69872,69882,69888,69941,69942,69959,69968,70007,70016,70094,70096,70112,70113,70133,70144,70162,70163,70207,70272,70279,70280,70281,70282,70286,70287,70302,70303,70314,70320,70379,70384,70394,70400,70404,70405,70413,70415,70417,70419,70441,70442,70449,70450,70452,70453,70458,70459,70469,70471,70473,70475,70478,70480,70481,70487,70488,70493,70500,70502,70509,70512,70517,70656,70746,70747,70748,70749,70751,70784,70856,70864,70874,71040,71094,71096,71134,71168,71237,71248,71258,71264,71277,71296,71352,71360,71370,71424,71451,71453,71468,71472,71488,71680,71740,71840,71923,71935,71936,72192,72264,72272,72324,72326,72355,72384,72441,72704,72713,72714,72759,72760,72774,72784,72813,72816,72848,72850,72872,72873,72887,72960,72967,72968,72970,72971,73015,73018,73019,73020,73022,73023,73032,73040,73050,73056,73062,73063,73065,73066,73103,73104,73106,73107,73113,73120,73130,73440,73465,73728,74650,74752,74863,74864,74869,74880,75076,77824,78895,82944,83527,92160,92729,92736,92767,92768,92778,92782,92784,92880,92910,92912,92918,92928,92998,93008,93018,93019,93026,93027,93048,93053,93072,93760,93851,93952,94021,94032,94079,94095,94112,94176,94178,94208,100338,100352,101107,110592,110879,110960,111356,113664,113771,113776,113789,113792,113801,113808,113818,113820,113824,118784,119030,119040,119079,119081,119155,119163,119273,119296,119366,119520,119540,119552,119639,119648,119673,119808,119893,119894,119965,119966,119968,119970,119971,119973,119975,119977,119981,119982,119994,119995,119996,119997,120004,120005,120070,120071,120075,120077,120085,120086,120093,120094,120122,120123,120127,120128,120133,120134,120135,120138,120145,120146,120486,120488,120780,120782,121484,121499,121504,121505,121520,122880,122887,122888,122905,122907,122914,122915,122917,122918,122923,124928,125125,125127,125143,125184,125259,125264,125274,125278,125280,126065,126133,126464,126468,126469,126496,126497,126499,126500,126501,126503,126504,126505,126515,126516,126520,126521,126522,126523,126524,126530,126531,126535,126536,126537,126538,126539,126540,126541,126544,126545,126547,126548,126549,126551,126552,126553,126554,126555,126556,126557,126558,126559,126560,126561,126563,126564,126565,126567,126571,126572,126579,126580,126584,126585,126589,126590,126591,126592,126602,126603,126620,126625,126628,126629,126634,126635,126652,126704,126706,126976,127020,127024,127124,127136,127151,127153,127168,127169,127184,127185,127222,127232,127245,127248,127340,127344,127405,127462,127491,127504,127548,127552,127561,127568,127570,127584,127590,127744,128725,128736,128749,128752,128762,128768,128884,128896,128985,129024,129036,129040,129096,129104,129114,129120,129160,129168,129198,129280,129292,129296,129343,129344,129393,129395,129399,129402,129403,129404,129443,129456,129466,129472,129475,129488,129536,129632,129646,131072,173783,173824,177973,177984,178206,178208,183970,183984,191457,194560,195102,917760,918000,}
diff --git a/script/luacheck/utils.lua b/script/luacheck/utils.lua
new file mode 100755
index 000000000..907c62975
--- /dev/null
+++ b/script/luacheck/utils.lua
@@ -0,0 +1,377 @@
+-- luacheck: push compat
+local unpack = table.unpack or unpack
+local pack = table.pack or function(...) return {n = select("#", ...), ...} end
+-- luacheck: pop
+
+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
+
+local class_metatable = {}
+
+function class_metatable.__call(class, ...)
+   local obj = setmetatable({}, class)
+
+   if class.__init then
+      local init_returns = pack(class.__init(obj, ...))
+
+      if init_returns.n > 0 then
+         return unpack(init_returns, 1, init_returns.n)
+      end
+   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.sorted_pairs(t)
+   local keys = {}
+
+   for key in pairs(t) do
+      table.insert(keys, key)
+   end
+
+   table.sort(keys)
+
+   local index = 1
+
+   return function()
+      local key = keys[index]
+
+      if key == nil then
+         return
+      end
+
+      index = index + 1
+
+      return key, t[key]
+   end
+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
+
+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 validator checking type.
+function utils.has_type(type_)
+   return function(x)
+      if type(x) == type_ then
+         return true
+      else
+         return false, ("%s expected, got %s"):format(type_, type(x))
+      end
+   end
+end
+
+-- Returns validator checking type and allowing false.
+function utils.has_type_or_false(type_)
+   return function(x)
+      if type(x) == type_ then
+         return true
+      elseif type(x) == "boolean" then
+         if x then
+            return false, ("%s or false expected, got true"):format(type_)
+         else
+            return true
+         end
+      else
+         return false, ("%s or false expected, got %s"):format(type_, type(x))
+      end
+   end
+end
+
+-- Returns validator checking two type possibilities.
+function utils.has_either_type(type1, type2)
+   return function(x)
+      if type(x) == type1 or type(x) == type2 then
+         return true
+      else
+         return false, ("%s or %s expected, got %s"):format(type1, type2, type(x))
+      end
+   end
+end
+
+-- Returns validator 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, ("array of %ss expected, got %s"):format(type_, type(x))
+      end
+
+      for index, item in ipairs(x) do
+         if type(item) ~= type_ then
+            return false, ("array of %ss expected, got %s at index [%d]"):format(type_, type(item), index)
+         end
+      end
+
+      return true
+   end
+end
+
+return utils
diff --git a/script/luacheck/vendor/sha1/LICENSE b/script/luacheck/vendor/sha1/LICENSE
new file mode 100755
index 000000000..0c4a9516b
--- /dev/null
+++ b/script/luacheck/vendor/sha1/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2013 Enrique García Cota, Eike Decker, Jeffrey Friedl
+Copyright (c) 2018 Peter Melnichenko
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/script/luacheck/vendor/sha1/bit32_ops.lua b/script/luacheck/vendor/sha1/bit32_ops.lua
new file mode 100755
index 000000000..344b0f364
--- /dev/null
+++ b/script/luacheck/vendor/sha1/bit32_ops.lua
@@ -0,0 +1,24 @@
+local bit32 = require "bit32"
+
+local ops = {}
+
+local band = bit32.band
+local bor = bit32.bor
+local bxor = bit32.bxor
+
+ops.uint32_lrot = bit32.lrotate
+ops.byte_xor = bxor
+ops.uint32_xor_3 = bxor
+ops.uint32_xor_4 = bxor
+
+function ops.uint32_ternary(a, b, c)
+   -- c ~ (a & (b ~ c)) has less bitwise operations than (a & b) | (~a & c).
+   return bxor(c, band(a, bxor(b, c)))
+end
+
+function ops.uint32_majority(a, b, c)
+   -- (a & (b | c)) | (b & c) has less bitwise operations than (a & b) | (a & c) | (b & c).
+   return bor(band(a, bor(b, c)), band(b, c))
+end
+
+return ops
diff --git a/script/luacheck/vendor/sha1/bit_ops.lua b/script/luacheck/vendor/sha1/bit_ops.lua
new file mode 100755
index 000000000..5fbaf056f
--- /dev/null
+++ b/script/luacheck/vendor/sha1/bit_ops.lua
@@ -0,0 +1,24 @@
+local bit = require "bit"
+
+local ops = {}
+
+local band = bit.band
+local bor = bit.bor
+local bxor = bit.bxor
+
+ops.uint32_lrot = bit.rol
+ops.byte_xor = bxor
+ops.uint32_xor_3 = bxor
+ops.uint32_xor_4 = bxor
+
+function ops.uint32_ternary(a, b, c)
+   -- c ~ (a & (b ~ c)) has less bitwise operations than (a & b) | (~a & c).
+   return bxor(c, band(a, bxor(b, c)))
+end
+
+function ops.uint32_majority(a, b, c)
+   -- (a & (b | c)) | (b & c) has less bitwise operations than (a & b) | (a & c) | (b & c).
+   return bor(band(a, bor(b, c)), band(b, c))
+end
+
+return ops
diff --git a/script/luacheck/vendor/sha1/common.lua b/script/luacheck/vendor/sha1/common.lua
new file mode 100755
index 000000000..c31ffef46
--- /dev/null
+++ b/script/luacheck/vendor/sha1/common.lua
@@ -0,0 +1,20 @@
+local common = {}
+
+-- Merges four bytes into a uint32 number.
+function common.bytes_to_uint32(a, b, c, d)
+   return a * 0x1000000 + b * 0x10000 + c * 0x100 + d
+end
+
+-- Splits a uint32 number into four bytes.
+function common.uint32_to_bytes(a)
+   local a4 = a % 256
+   a = (a - a4) / 256
+   local a3 = a % 256
+   a = (a - a3) / 256
+   local a2 = a % 256
+   local a1 = (a - a2) / 256
+   return a1, a2, a3, a4
+end
+
+
+return common
diff --git a/script/luacheck/vendor/sha1/init.lua b/script/luacheck/vendor/sha1/init.lua
new file mode 100755
index 000000000..49f95bee0
--- /dev/null
+++ b/script/luacheck/vendor/sha1/init.lua
@@ -0,0 +1,195 @@
+local common = require "luacheck.vendor.sha1.common"
+
+local sha1 = {
+   -- Meta fields retained for compatibility.
+   _VERSION     = "sha.lua 0.6.0",
+   _URL         = "https://github.com/mpeterv/sha1",
+   _DESCRIPTION = [[
+SHA-1 secure hash and HMAC-SHA1 signature computation in Lua,
+using bit and bit32 modules and Lua 5.3 operators when available
+and falling back to a pure Lua implementation on Lua 5.1.
+Based on code orignally by Jeffrey Friedl and modified by
+Eike Decker and Enrique García Cota.]],
+   _LICENSE = [[
+MIT LICENSE
+
+Copyright (c) 2013 Enrique García Cota, Eike Decker, Jeffrey Friedl
+Copyright (c) 2018 Peter Melnichenko
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.]]
+}
+
+sha1.version = "0.6.0"
+
+local function choose_ops()
+   if _VERSION:find("5%.3") then
+      return "lua53_ops"
+   elseif pcall(require, "bit") then
+      return "bit_ops"
+   elseif pcall(require, "bit32") then
+      return "bit32_ops"
+   else
+      return "pure_lua_ops"
+   end
+end
+
+local ops = require("luacheck.vendor.sha1." .. choose_ops())
+local uint32_lrot = ops.uint32_lrot
+local byte_xor = ops.byte_xor
+local uint32_xor_3 = ops.uint32_xor_3
+local uint32_xor_4 = ops.uint32_xor_4
+local uint32_ternary = ops.uint32_ternary
+local uint32_majority = ops.uint32_majority
+
+local bytes_to_uint32 = common.bytes_to_uint32
+local uint32_to_bytes = common.uint32_to_bytes
+
+local sbyte = string.byte
+local schar = string.char
+local sformat = string.format
+local srep = string.rep
+
+local function hex_to_binary(hex)
+   return (hex:gsub("..", function(hexval)
+      return schar(tonumber(hexval, 16))
+   end))
+end
+
+-- Calculates SHA1 for a string, returns it encoded as 40 hexadecimal digits.
+function sha1.sha1(str)
+   -- Input preprocessing.
+   -- First, append a `1` bit and seven `0` bits.
+   local first_append = schar(0x80)
+
+   -- Next, append some zero bytes to make the length of the final message a multiple of 64.
+   -- Eight more bytes will be added next.
+   local non_zero_message_bytes = #str + 1 + 8
+   local second_append = srep(schar(0), -non_zero_message_bytes % 64)
+
+   -- Finally, append the length of the original message in bits as a 64-bit number.
+   -- Assume that it fits into the lower 32 bits.
+   local third_append = schar(0, 0, 0, 0, uint32_to_bytes(#str * 8))
+
+   str = str .. first_append .. second_append .. third_append
+   assert(#str % 64 == 0)
+
+   -- Initialize hash value.
+   local h0 = 0x67452301
+   local h1 = 0xEFCDAB89
+   local h2 = 0x98BADCFE
+   local h3 = 0x10325476
+   local h4 = 0xC3D2E1F0
+
+   local w = {}
+
+   -- Process the input in successive 64-byte chunks.
+   for chunk_start = 1, #str, 64 do
+      -- Load the chunk into W[0..15] as uint32 numbers.
+      local uint32_start = chunk_start
+
+      for i = 0, 15 do
+         w[i] = bytes_to_uint32(sbyte(str, uint32_start, uint32_start + 3))
+         uint32_start = uint32_start + 4
+      end
+
+      -- Extend the input vector.
+      for i = 16, 79 do
+         w[i] = uint32_lrot(uint32_xor_4(w[i - 3], w[i - 8], w[i - 14], w[i - 16]), 1)
+      end
+
+      -- Initialize hash value for this chunk.
+      local a = h0
+      local b = h1
+      local c = h2
+      local d = h3
+      local e = h4
+
+      -- Main loop.
+      for i = 0, 79 do
+         local f
+         local k
+
+         if i <= 19 then
+            f = uint32_ternary(b, c, d)
+            k = 0x5A827999
+         elseif i <= 39 then
+            f = uint32_xor_3(b, c, d)
+            k = 0x6ED9EBA1
+         elseif i <= 59 then
+            f = uint32_majority(b, c, d)
+            k = 0x8F1BBCDC
+         else
+            f = uint32_xor_3(b, c, d)
+            k = 0xCA62C1D6
+         end
+
+         local temp = (uint32_lrot(a, 5) + f + e + k + w[i]) % 0x100000000
+         e = d
+         d = c
+         c = uint32_lrot(b, 30)
+         b = a
+         a = temp
+      end
+
+      -- Add this chunk's hash to result so far.
+      h0 = (h0 + a) % 0x100000000
+      h1 = (h1 + b) % 0x100000000
+      h2 = (h2 + c) % 0x100000000
+      h3 = (h3 + d) % 0x100000000
+      h4 = (h4 + e) % 0x100000000
+   end
+
+   return sformat("%08x%08x%08x%08x%08x", h0, h1, h2, h3, h4)
+end
+
+function sha1.binary(str)
+   return hex_to_binary(sha1.sha1(str))
+end
+
+-- Precalculate replacement tables.
+local xor_with_0x5c = {}
+local xor_with_0x36 = {}
+
+for i = 0, 0xff do
+   xor_with_0x5c[schar(i)] = schar(byte_xor(0x5c, i))
+   xor_with_0x36[schar(i)] = schar(byte_xor(0x36, i))
+end
+
+-- 512 bits.
+local BLOCK_SIZE = 64
+
+function sha1.hmac(key, text)
+   if #key > BLOCK_SIZE then
+      key = sha1.binary(key)
+   end
+
+   local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. srep(schar(0x36), BLOCK_SIZE - #key)
+   local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. srep(schar(0x5c), BLOCK_SIZE - #key)
+
+   return sha1.sha1(key_xord_with_0x5c .. sha1.binary(key_xord_with_0x36 .. text))
+end
+
+function sha1.hmac_binary(key, text)
+   return hex_to_binary(sha1.hmac(key, text))
+end
+
+setmetatable(sha1, {__call = function(_, str) return sha1.sha1(str) end})
+
+return sha1
diff --git a/script/luacheck/vendor/sha1/lua53_ops.lua b/script/luacheck/vendor/sha1/lua53_ops.lua
new file mode 100755
index 000000000..d5312e6b5
--- /dev/null
+++ b/script/luacheck/vendor/sha1/lua53_ops.lua
@@ -0,0 +1,29 @@
+local ops = {}
+
+function ops.uint32_lrot(a, bits)
+   return ((a << bits) & 0xFFFFFFFF) | (a >> (32 - bits))
+end
+
+function ops.byte_xor(a, b)
+   return a ~ b
+end
+
+function ops.uint32_xor_3(a, b, c)
+   return a ~ b ~ c
+end
+
+function ops.uint32_xor_4(a, b, c, d)
+   return a ~ b ~ c ~ d
+end
+
+function ops.uint32_ternary(a, b, c)
+   -- c ~ (a & (b ~ c)) has less bitwise operations than (a & b) | (~a & c).
+   return c ~ (a & (b ~ c))
+end
+
+function ops.uint32_majority(a, b, c)
+   -- (a & (b | c)) | (b & c) has less bitwise operations than (a & b) | (a & c) | (b & c).
+   return (a & (b | c)) | (b & c)
+end
+
+return ops
diff --git a/script/luacheck/vendor/sha1/pure_lua_ops.lua b/script/luacheck/vendor/sha1/pure_lua_ops.lua
new file mode 100755
index 000000000..8f255cb79
--- /dev/null
+++ b/script/luacheck/vendor/sha1/pure_lua_ops.lua
@@ -0,0 +1,144 @@
+local common = require "luacheck.vendor.sha1.common"
+
+local ops = {}
+
+local bytes_to_uint32 = common.bytes_to_uint32
+local uint32_to_bytes = common.uint32_to_bytes
+
+function ops.uint32_lrot(a, bits)
+   local power = 2 ^ bits
+   local inv_power = 0x100000000 / power
+   local lower_bits = a % inv_power
+   return (lower_bits * power) + ((a - lower_bits) / inv_power)
+end
+
+-- Build caches for bitwise `and` and `xor` over bytes to speed up uint32 operations.
+-- Building the cache by simply applying these operators over all pairs is too slow and
+-- duplicates a lot of work over different bits of inputs.
+-- Instead, when building a cache over bytes, for each pair of bytes split both arguments
+-- into two 4-bit numbers, calculate values over these two halves, then join the results into a byte again.
+-- While there are 256 * 256 = 65536 pairs of bytes, there are only 16 * 16 = 256 pairs
+-- of 4-bit numbers, so that building an 8-bit cache given a 4-bit cache is rather efficient.
+-- The same logic is applied recursively to make a 4-bit cache from a 2-bit cache and a 2-bit
+-- cache from a 1-bit cache, which is calculated given the 1-bit version of the operator.
+
+-- Returns a cache containing all values of a bitwise operator over numbers with given number of bits,
+-- given an operator over single bits.
+-- Value of `op(a, b)` is stored in `cache[a * (2 ^ bits) + b]`.
+local function make_op_cache(bit_op, bits)
+   if bits == 1 then
+      return {[0] = bit_op(0, 0), bit_op(0, 1), bit_op(1, 0), bit_op(1, 1)}
+   end
+
+   local half_bits = bits / 2
+   local size = 2 ^ bits
+   local half_size = 2 ^ half_bits
+   local half_cache = make_op_cache(bit_op, half_bits)
+
+   local cache = {}
+
+   -- The implementation used is an optimized version of the following reference one,
+   -- with intermediate calculations reused and moved to the outermost loop possible.
+   -- It's possible to reorder the loops and move the calculation of one of the
+   -- half-results one level up, but then the cache is not filled in a proper array order
+   -- and its access performance suffers.
+
+   -- for a1 = 0, half_size - 1 do
+   --    for a2 = 0, half_size - 1 do
+   --       for b1 = 0, half_size - 1 do
+   --          for b2 = 0, half_size - 1 do
+   --             local a = a1 * half_size + a2
+   --             local b = b1 * half_size + b2
+   --             local v1 = half_cache[a1 * half_size + b1]
+   --             local v2 = half_cache[a2 * half_size + b2]
+   --             local v = v1 * half_size + v2
+   --             cache[a * size + b] = v
+   --          end
+   --       end
+   --    end
+   -- end
+
+   for a1 = 0, half_size - 1 do
+      local a1_half_size = a1 * half_size
+
+      for a2 = 0, half_size - 1 do
+         local a2_size = a2 * half_size
+         local a_size = (a1_half_size + a2) * size
+
+         for b1 = 0, half_size - 1 do
+            local a_size_plus_b1_half_size = a_size + b1 * half_size
+            local v1_half_size = half_cache[a1_half_size + b1] * half_size
+
+            for b2 = 0, half_size - 1 do
+               cache[a_size_plus_b1_half_size + b2] = v1_half_size + half_cache[a2_size + b2]
+            end
+         end
+      end
+   end
+
+   return cache
+end
+
+local byte_and_cache = make_op_cache(function(a, b) return a * b end, 8)
+local byte_xor_cache = make_op_cache(function(a, b) return a == b and 0 or 1 end, 8)
+
+function ops.byte_xor(a, b)
+   return byte_xor_cache[a * 256 + b]
+end
+
+function ops.uint32_xor_3(a, b, c)
+   local a1, a2, a3, a4 = uint32_to_bytes(a)
+   local b1, b2, b3, b4 = uint32_to_bytes(b)
+   local c1, c2, c3, c4 = uint32_to_bytes(c)
+
+   return bytes_to_uint32(
+      byte_xor_cache[a1 * 256 + byte_xor_cache[b1 * 256 + c1]],
+      byte_xor_cache[a2 * 256 + byte_xor_cache[b2 * 256 + c2]],
+      byte_xor_cache[a3 * 256 + byte_xor_cache[b3 * 256 + c3]],
+      byte_xor_cache[a4 * 256 + byte_xor_cache[b4 * 256 + c4]]
+   )
+end
+
+function ops.uint32_xor_4(a, b, c, d)
+   local a1, a2, a3, a4 = uint32_to_bytes(a)
+   local b1, b2, b3, b4 = uint32_to_bytes(b)
+   local c1, c2, c3, c4 = uint32_to_bytes(c)
+   local d1, d2, d3, d4 = uint32_to_bytes(d)
+
+   return bytes_to_uint32(
+      byte_xor_cache[a1 * 256 + byte_xor_cache[b1 * 256 + byte_xor_cache[c1 * 256 + d1]]],
+      byte_xor_cache[a2 * 256 + byte_xor_cache[b2 * 256 + byte_xor_cache[c2 * 256 + d2]]],
+      byte_xor_cache[a3 * 256 + byte_xor_cache[b3 * 256 + byte_xor_cache[c3 * 256 + d3]]],
+      byte_xor_cache[a4 * 256 + byte_xor_cache[b4 * 256 + byte_xor_cache[c4 * 256 + d4]]]
+   )
+end
+
+function ops.uint32_ternary(a, b, c)
+   local a1, a2, a3, a4 = uint32_to_bytes(a)
+   local b1, b2, b3, b4 = uint32_to_bytes(b)
+   local c1, c2, c3, c4 = uint32_to_bytes(c)
+
+   -- (a & b) + (~a & c) has less bitwise operations than (a & b) | (~a & c).
+   return bytes_to_uint32(
+      byte_and_cache[b1 * 256 + a1] + byte_and_cache[c1 * 256 + 255 - a1],
+      byte_and_cache[b2 * 256 + a2] + byte_and_cache[c2 * 256 + 255 - a2],
+      byte_and_cache[b3 * 256 + a3] + byte_and_cache[c3 * 256 + 255 - a3],
+      byte_and_cache[b4 * 256 + a4] + byte_and_cache[c4 * 256 + 255 - a4]
+   )
+end
+
+function ops.uint32_majority(a, b, c)
+   local a1, a2, a3, a4 = uint32_to_bytes(a)
+   local b1, b2, b3, b4 = uint32_to_bytes(b)
+   local c1, c2, c3, c4 = uint32_to_bytes(c)
+
+   -- (a & b) + (c & (a ~ b)) has less bitwise operations than (a & b) | (a & c) | (b & c).
+   return bytes_to_uint32(
+      byte_and_cache[a1 * 256 + b1] + byte_and_cache[c1 * 256 + byte_xor_cache[a1 * 256 + b1]],
+      byte_and_cache[a2 * 256 + b2] + byte_and_cache[c2 * 256 + byte_xor_cache[a2 * 256 + b2]],
+      byte_and_cache[a3 * 256 + b3] + byte_and_cache[c3 * 256 + byte_xor_cache[a3 * 256 + b3]],
+      byte_and_cache[a4 * 256 + b4] + byte_and_cache[c4 * 256 + byte_xor_cache[a4 * 256 + b4]]
+   )
+end
+
+return ops
diff --git a/script/luacheck/version.lua b/script/luacheck/version.lua
new file mode 100755
index 000000000..f4af858b2
--- /dev/null
+++ b/script/luacheck/version.lua
@@ -0,0 +1,36 @@
+local argparse = require "argparse"
+local lfs = require "lfs"
+local luacheck = require "luacheck"
+local multithreading = require "luacheck.multithreading"
+local utils = require "luacheck.utils"
+
+local version = {}
+
+version.luacheck = luacheck._VERSION
+
+if rawget(_G, "jit") then
+   version.lua = rawget(_G, "jit").version
+elseif _VERSION:find("^Lua ") then
+   version.lua = "PUC-Rio " .. _VERSION
+else
+   version.lua = _VERSION
+end
+
+version.argparse = argparse.version
+
+version.lfs = utils.unprefix(lfs._VERSION, "LuaFileSystem ")
+
+if multithreading.has_lanes then
+   version.lanes = multithreading.lanes.ABOUT.version
+else
+   version.lanes = "Not found"
+end
+
+version.string = ([[
+Luacheck: %s
+Lua: %s
+Argparse: %s
+LuaFileSystem: %s
+LuaLanes: %s]]):format(version.luacheck, version.lua, version.argparse, version.lfs, version.lanes)
+
+return version
diff --git a/script/parser/compile.lua b/script/parser/compile.lua
index 8dd772dbe..da556f0ec 100644
--- a/script/parser/compile.lua
+++ b/script/parser/compile.lua
@@ -1275,7 +1275,7 @@ local function parseNumber2(start)
             finish  = getPosition(offset - 1, 'right'),
             version = 'LuaJIT',
             info    = {
-                version = 'Lua 5.4',
+                version = State.version,
             }
         }
     end
diff --git a/script/proto/diagnostic.lua b/script/proto/diagnostic.lua
index 61b8ff4bd..827f914c4 100644
--- a/script/proto/diagnostic.lua
+++ b/script/proto/diagnostic.lua
@@ -39,17 +39,9 @@ function m.register(names)
 end
 
 m.register {
-    'unused-local',
-    'unused-function',
-    'unused-label',
-    'unused-vararg',
-    'trailing-space',
-    'redundant-return',
-    'empty-block',
-    'code-after-break',
-    'unreachable-code',
+    'luacheck',
 } {
-    group    = 'unused',
+    group    = 'luacheck',
     severity = 'Hint',
     status   = 'Opened',
 }
diff --git a/script/provider/diagnostic.lua b/script/provider/diagnostic.lua
index 37c88bb67..19ae27911 100644
--- a/script/provider/diagnostic.lua
+++ b/script/provider/diagnostic.lua
@@ -606,19 +606,19 @@ function m.pullDiagnosticScope(callback)
         and config.get(scp.uri, 'Lua.diagnostics.enable') then
             local id = 'diagnosticsScope:' .. scp:getName()
             await.close(id)
-            await.call(function () ---@async
+            await.await(function () ---@async
                 processing = processing + 1
                 local _ <close> = util.defer(function ()
                     processing = processing - 1
                 end)
 
-                local delay = config.get(scp.uri, 'Lua.diagnostics.workspaceDelay') / 1000
-                if delay < 0 then
-                    return
-                end
-                print(delay)
-                await.sleep(math.max(delay, 0.2))
-                print('start')
+                -- local delay = config.get(scp.uri, 'Lua.diagnostics.workspaceDelay') / 1000
+                -- if delay < 0 then
+                --     return
+                -- end
+                -- print(delay)
+                -- await.sleep(math.max(delay, 0.2))
+                -- print('start')
 
                 m.awaitDiagnosticsScope(scp.uri, function (fileUri)
                     local suc, result, unchanged = xpcall(m.pullDiagnostic, log.error, fileUri, true)
@@ -634,11 +634,7 @@ function m.pullDiagnosticScope(callback)
             end, id)
         end
     end
-
-    -- sleep for ever
-    while true do
-        await.sleep(1.0)
-    end
+    await.delay()
 end
 
 function m.refreshClient()
diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index 69fb3263a..3b79ef21a 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -1437,7 +1437,7 @@ m.register 'inlayHint/resolve' {
 }
 
 m.register 'textDocument/diagnostic' {
-    preview = true,
+    -- preview = true,
     capability = {
         diagnosticProvider = {
             identifier            = 'identifier',
@@ -1450,40 +1450,33 @@ m.register 'textDocument/diagnostic' {
         local uri = files.getRealUri(params.textDocument.uri)
         workspace.awaitReady(uri)
         local core = require 'provider.diagnostic'
-        -- TODO: do some trick
-        core.doDiagnostic(uri)
 
-        return {
-            kind = 'unchanged',
-            resultId = uri,
-        }
-
-        --if not params.previousResultId then
-        --    core.clearCache(uri)
-        --end
-        --local results, unchanged = core.pullDiagnostic(uri, false)
-        --if unchanged then
-        --    return {
-        --        kind = 'unchanged',
-        --        resultId = uri,
-        --    }
-        --else
-        --    return {
-        --        kind = 'full',
-        --        resultId = uri,
-        --        items = results or {},
-        --    }
-        --end
+        if not params.previousResultId then
+           core.clear(uri)
+        end
+        local results, unchanged = core.pullDiagnostic(uri, false)
+        if unchanged then
+           return {
+               kind = 'unchanged',
+               resultId = uri,
+           }
+        else
+           return {
+               kind = 'full',
+               resultId = uri,
+               items = results or {},
+           }
+        end
     end
 }
 
 m.register 'workspace/diagnostic' {
     --preview = true,
-    --capability = {
-    --    diagnosticProvider = {
-    --        workspaceDiagnostics  = false,
-    --    }
-    --},
+    capability = {
+       diagnosticProvider = {
+           workspaceDiagnostics = true,
+       }
+    },
     ---@async
     function (params)
         local core = require 'provider.diagnostic'
@@ -1492,6 +1485,7 @@ m.register 'workspace/diagnostic' {
             excepts[#excepts+1] = id.value
         end
         core.clearCacheExcept(excepts)
+        local items = {}
         local function convertItem(result)
             if result.unchanged then
                 return {
@@ -1510,17 +1504,20 @@ m.register 'workspace/diagnostic' {
                 }
             end
         end
-        core.pullDiagnosticScope(function (result)
-            proto.notify('$/progress', {
-                token = params.partialResultToken,
-                value = {
-                    items = {
-                        convertItem(result)
-                    }
-                }
-            })
+        await.await(function()
+            core.pullDiagnosticScope(function (result)
+                table.insert(items, convertItem(result))
+                -- proto.notify('$/progress', {
+                --     token = params.partialResultToken,
+                --     value = {
+                --         items = {
+                --             convertItem(result)
+                --         }
+                --     }
+                -- })
+            end)
         end)
-        return { items = {} }
+        return { items = items }
     end
 }
 
