X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=runtime%2Fplugins%2Flinter%2Flinter.lua;h=546f5779b68d4a249bd612e84ac78ada3079af19;hb=8a907956d1701aa9675a26bb587cd282562c988f;hp=20d85e08a6d67ac0a7b9817408cdd989772db508;hpb=fc9ddaf94187a47732f02051f7ce52f9eec11ff1;p=micro.git diff --git a/runtime/plugins/linter/linter.lua b/runtime/plugins/linter/linter.lua index 20d85e08..546f5779 100644 --- a/runtime/plugins/linter/linter.lua +++ b/runtime/plugins/linter/linter.lua @@ -1,82 +1,179 @@ -if GetOption("linter") == nil then - AddOption("linter", true) -end +VERSION = "1.0.0" + +local micro = import("micro") +local runtime = import("runtime") +local filepath = import("path/filepath") +local shell = import("micro/shell") +local buffer = import("micro/buffer") +local config = import("micro/config") -MakeCommand("lint", "linter.lintCommand", 0) +local linters = {} -function lintCommand() - CurView():Save(false) - runLinter() +-- creates a linter entry, call from within an initialization function, not +-- directly at initial load time +-- +-- name: name of the linter +-- filetype: filetype to check for to use linter +-- cmd: main linter process that is executed +-- args: arguments to pass to the linter process +-- use %f to refer to the current file name +-- use %d to refer to the current directory name +-- errorformat: how to parse the linter/compiler process output +-- %f: file, %l: line number, %m: error/warning message +-- os: list of OSs this linter is supported or unsupported on +-- optional param, default: {} +-- whitelist: should the OS list be a blacklist (do not run the linter for these OSs) +-- or a whitelist (only run the linter for these OSs) +-- optional param, default: false (should blacklist) +-- domatch: should the filetype be interpreted as a lua pattern to match with +-- the actual filetype, or should the linter only activate on an exact match +-- optional param, default: false (require exact match) +-- loffset: line offset will be added to the line number returned by the linter +-- useful if the linter returns 0-indexed lines +-- optional param, default: 0 +-- coffset: column offset will be added to the col number returned by the linter +-- useful if the linter returns 0-indexed columns +-- optional param, default: 0 +-- callback: function to call before executing the linter, if it returns +-- false the lint is canceled. The callback is passed the buf. +-- optional param, default: nil +function makeLinter(name, filetype, cmd, args, errorformat, os, whitelist, domatch, loffset, coffset, callback) + if linters[name] == nil then + linters[name] = {} + linters[name].filetype = filetype + linters[name].cmd = cmd + linters[name].args = args + linters[name].errorformat = errorformat + linters[name].os = os or {} + linters[name].whitelist = whitelist or false + linters[name].domatch = domatch or false + linters[name].loffset = loffset or 0 + linters[name].coffset = coffset or 0 + linters[name].callback = callback or nil + end end -function runLinter() - local ft = CurView().Buf:FileType() - local file = CurView().Buf.Path - local dir = DirectoryName(file) - if OS == "windows" then +function removeLinter(name) + linters[name] = nil +end + +function init() + local devnull = "/dev/null" + if runtime.GOOS == "windows" then devnull = "NUL" - else - devnull = "/dev/null" end - if ft == "go" then - lint("gobuild", "go", {"build", "-o", devnull}, "%f:%l: %m") - lint("golint", "golint", {file}, "%f:%l:%d+: %m") - elseif ft == "lua" then - lint("luacheck", "luacheck", {"--no-color", file}, "%f:%l:%d+: %m") - elseif ft == "python" then - lint("pyflakes", "pyflakes", {file}, "%f:%l:.-:? %m") - lint("mypy", "mypy", {file}, "%f:%l: %m") - lint("pylint", "pylint", {"--output-format=parseable", "--reports=no", file}, "%f:%l: %m") - elseif ft == "c" then - lint("gcc", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m") - elseif ft == "c++" then - lint("gcc", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m") - elseif ft == "swift" and OS == "darwin" then - lint("switfc", "xcrun", {"swiftc", file}, "%f:%l:%d+:.+: %m") - elseif ft == "swift" and OS == "linux" then - lint("switfc", "swiftc", {file}, "%f:%l:%d+:.+: %m") - elseif ft == "Objective-C" then - lint("clang", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m") - elseif ft == "d" then - lint("dmd", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", file}, "%f%(%l%):.+: %m") - elseif ft == "java" then - lint("javac", "javac", {"-d", dir, file}, "%f:%l: error: %m") - elseif ft == "javascript" then - lint("jshint", "jshint", {file}, "%f: line %l,.+, %m") - elseif ft == "nim" then - lint("nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", file}, "%f.%l, %d+. %m") - elseif string.match(ft, "literate") then - lint("literate", "lit", {"-c", file}, "%f:%l:%m") - elseif ft == "yaml" then - lint("yaml", "yamllint", {"--format", "parsable", file}, "%f:%l:%d+:.+ %m") + + makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m") + makeLinter("gcc", "c++", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m") + makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m") + makeLinter("gobuild", "go", "go", {"build", "-o", devnull}, "%f:%l:%c:? %m") + -- makeLinter("golint", "go", "golint", {"%f"}, "%f:%l:%c: %m") + makeLinter("javac", "java", "javac", {"-d", "%d", "%f"}, "%f:%l: error: %m") + makeLinter("jshint", "javascript", "jshint", {"%f"}, "%f: line %l,.+, %m") + makeLinter("literate", "literate", "lit", {"-c", "%f"}, "%f:%l:%m", {}, false, true) + makeLinter("luacheck", "lua", "luacheck", {"--no-color", "%f"}, "%f:%l:%c: %m") + makeLinter("nim", "nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", "%f"}, "%f.%l, %c. %m") + makeLinter("clang", "objective-c", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m") + makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m") + makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m") + makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m") + makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m") + makeLinter("swiftc", "swift", "xcrun", {"swiftc", "%f"}, "%f:%l:%c:.+: %m", {"darwin"}, true) + makeLinter("swiftc", "swiftc", {"%f"}, "%f:%l:%c:.+: %m", {"linux"}, true) + makeLinter("yaml", "yaml", "yamllint", {"--format", "parsable", "%f"}, "%f:%l:%c:.+ %m") + + config.MakeCommand("lint", function(bp, args) + bp:Save() + runLinter(bp.Buf) + end, config.NoComplete) + + config.AddRuntimeFile("linter", config.RTHelp, "help/linter.md") +end + +function contains(list, element) + for k, v in pairs(list) do + if v == element then + return true + end end + return false end -function onSave(view) - if GetOption("linter") then - runLinter() - else - CurView():ClearAllGutterMessages() +function runLinter(buf) + local ft = buf:FileType() + local file = buf.Path + local dir = filepath.Dir(file) + + for k, v in pairs(linters) do + local ftmatch = ft == v.filetype + if v.domatch then + ftmatch = string.match(ft, v.filetype) + end + + local hasOS = contains(v.os, runtime.GOOS) + if not hasOS and v.whitelist then + ftmatch = false + end + if hasOS and not v.whitelist then + ftmatch = false + end + + local args = {} + for k, arg in pairs(v.args) do + args[k] = arg:gsub("%%f", file):gsub("%%d", dir) + end + + if ftmatch then + lint(buf, k, v.cmd, args, v.errorformat, v.loffset, v.coffset, v.callback) + end end end -function lint(linter, cmd, args, errorformat) - CurView():ClearGutterMessages(linter) +function onSave(bp) + runLinter(bp.Buf) + return true +end + +function lint(buf, linter, cmd, args, errorformat, loff, coff, callback) + buf:ClearMessages(linter) + + if callback ~= nil then + if not callback(buf) then + return + end + end - JobSpawn(cmd, args, "", "", "linter.onExit", linter, errorformat) + shell.JobSpawn(cmd, args, nil, nil, onExit, buf, linter, errorformat, loff, coff) end -function onExit(output, linter, errorformat) +function onExit(output, args) + local buf, linter, errorformat, loff, coff = args[1], args[2], args[3], args[4], args[5] local lines = split(output, "\n") - local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%m", "(.+)") + local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%c", "(%d+)"):gsub("%%m", "(.+)") for _,line in ipairs(lines) do -- Trim whitespace line = line:match("^%s*(.+)%s*$") if string.find(line, regex) then - local file, line, msg = string.match(line, regex) - if basename(CurView().Buf.Path) == basename(file) then - CurView():GutterMessage(linter, tonumber(line), msg, 2) + local file, line, col, msg = string.match(line, regex) + local hascol = true + if not string.find(errorformat, "%%c") then + hascol = false + msg = col + elseif col == nil then + hascol = false + end + micro.Log(basename(buf.Path), basename(file)) + if basename(buf.Path) == basename(file) then + local bmsg = nil + if hascol then + local mstart = buffer.Loc(tonumber(col-1+coff), tonumber(line-1+loff)) + local mend = buffer.Loc(tonumber(col+coff), tonumber(line-1+loff)) + bmsg = buffer.NewMessage(linter, msg, mstart, mend, buffer.MTError) + else + bmsg = buffer.NewMessageAtLine(linter, msg, tonumber(line+loff), buffer.MTError) + end + buf:AddMessage(bmsg) end end end @@ -93,7 +190,7 @@ end function basename(file) local sep = "/" - if OS == "windows" then + if runtime.GOOS == "windows" then sep = "\\" end local name = string.gsub(file, "(.*" .. sep .. ")(.*)", "%2")