#!/usr/bin/env lua --[[ Based on luacheck: https://github.com/luarocks/luacheck The MIT License (MIT) Copyright (c) 2014 - 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. ]] -- Monkey patch Luacheck (tested against version 0.23.0) to support Candran files local candran = require("candran") local util = require("candran.util") -- set a function upvalues (if several names are given, will go up the upvalue chain) local function setupvalue(fn, val, name, ...) for i=1, debug.getinfo(fn, "u").nups do local n, v = debug.getupvalue(fn, i) if n == name then if not ... then debug.setupvalue(fn, i, val) else setupvalue(v, val, ...) end end end end -- escape a string to be used as a pattern local function escape(str) return str:gsub("[^%w]", "%%%0") end -- returns a pattern that find start and stop position of a token local function pattern(token) return "()"..escape(token).."()" end -- token aliases local tokenAlias = { ["self"] = { "@", ":" } } -- Patch checker local oldCheck = require("luacheck.check") local function check(can) local lua, err = candran.make(can, {chunkname="_luacheck_source"}) -- Warnings if lua then local r = oldCheck(lua) -- Calculate Candran file position. if #r.warnings > 0 then local lua_lines = {} for l in (lua.."\n"):gmatch("([^\n]*)\n") do table.insert(lua_lines, l) end local can_lines = {} for l in (can.."\n"):gmatch("([^\n]*)\n") do table.insert(can_lines, l) end for i=#r.warnings, 1, -1 do local warning = r.warnings[i] -- calculating candran line local lua_line = lua_lines[warning.line] local source, line = lua_line:match(".*%-%- (.-)%:(%d+)$") if source ~= "_luacheck_source" then -- line is from another file, discard table.remove(r.warnings, i) elseif source then warning.can_line = tonumber(line) -- do the same for prev_line if warning.prev_line then local s, l = lua_lines[warning.prev_line]:match(".*%-%- (.-)%:(%d+)$") if s ~= "_luacheck_source" then warning.prev_line = s..":"..l -- luacheck seems to do no validation on this, so we can redefine it to anything elseif l then warning.prev_line = l end end -- Errors codes highlighting a identifier (alphanumeric+underscore not starting with a number): -- 1xx: global variables -- 2xx: unused variables -- 3xx: unused values -- 4xx: shadowing delarations local isIdentifier = tonumber(warning.code) >= 100 and tonumber(warning.code) < 500 -- calculating candran column local can_line = can_lines[warning.can_line] local lua_token = lua_line:sub(warning.column, warning.end_column) local token_pattern = pattern(lua_token) -- token finding pattern -- the warning happens on the n-th instance of lua_token on this line local lua_n = 1 for start in lua_line:gmatch(token_pattern) do if start >= warning.column then break end lua_n = lua_n + 1 end -- Find associated candran token. If lua_n > can_nmax, the last found lua_token is used. -- This approximation should work in like, 90% of cases. local can_n = 1 local pos = 1 while can_n <= lua_n do -- find first token or alias of this token local start, stop = can_line:match(token_pattern, pos) if tokenAlias[lua_token] then for _, token in ipairs(tokenAlias[lua_token]) do local nstart, nstop = can_line:match(pattern(token), pos) if nstart and (not start or nstart < start) then start, stop = nstart, nstop end end -- for non aliases token that are identifier, check if match is a full identifier -- (avoid things like `let e` matching the e in let) elseif start and isIdentifier then local prev = can_line:sub(start-1, start-1) local next = can_line:sub(stop, stop) if prev:match("[%w_]") or next:match("[%w_]") then can_n = can_n - 1 -- skip this match end end -- found if start then pos = stop warning.can_column, warning.can_end_column = start, stop-1 can_n = can_n + 1 else break end end -- AFAIK, prev_column and prev_end_column are not displayed in any warning so we don't need to recalculate them for Candran. end end end return r -- Syntax error else local line, column, msg = err:match(":(%d+):(%d+):%s*(.*)$") local syntax_error = { code = "011", line = line, column = column, end_column = column, msg = msg } return { warnings = {syntax_error}, inline_options = {}, line_lengths = {}, line_endings = {} } end end package.loaded["luacheck.check"] = check local runner = require("luacheck.runner") local oldRunner = runner.new function runner.new(opts) -- Disable max line length checking (it is compiled code...) opts.max_line_length = false return oldRunner(opts) end -- Patch formatter local format = require("luacheck.format") local function format_location(file, location, opts) local res = ("%s:%d:%d"):format(file, location.can_line or location.line, location.can_column or location.column) if opts.ranges then res = ("%s-%d"):format(res, location.can_end_column or location.end_column) end return res end setupvalue(format.builtin_formatters.plain, format_location, "format_event", "format_location") -- Fix some Luacheck messages and run local path = util.search("luacheck.main", {"lua"}) if path then local f = io.open(path, "r") local code = f:read("*a") f:close() code = code:gsub(escape(" bug (please report at https://github.com/mpeterv/luacheck/issues)"), ", patched for Candran "..candran.VERSION.." bug. Please DO NOT report this bug to Luacheck") -- error text :gsub(escape("\"luacheck\","), "\"cancheck\",") -- command name :gsub("a linter and a static analyzer for Lua%.", "a linter and a static analyzer for Lua, patched for Candran "..candran.VERSION..".") -- help text -- run return load(code)() else io.stderr:write("can't find luacheck.main\n") os.exit(1) end