3 local micro = import("micro")
4 local runtime = import("runtime")
5 local filepath = import("path/filepath")
6 local shell = import("micro/shell")
7 local buffer = import("micro/buffer")
8 local config = import("micro/config")
12 -- creates a linter entry, call from within an initialization function, not
13 -- directly at initial load time
15 -- name: name of the linter
16 -- filetype: filetype to check for to use linter
17 -- cmd: main linter process that is executed
18 -- args: arguments to pass to the linter process
19 -- use %f to refer to the current file name
20 -- use %d to refer to the current directory name
21 -- errorformat: how to parse the linter/compiler process output
22 -- %f: file, %l: line number, %m: error/warning message
23 -- os: list of OSs this linter is supported or unsupported on
24 -- optional param, default: {}
25 -- whitelist: should the OS list be a blacklist (do not run the linter for these OSs)
26 -- or a whitelist (only run the linter for these OSs)
27 -- optional param, default: false (should blacklist)
28 -- domatch: should the filetype be interpreted as a lua pattern to match with
29 -- the actual filetype, or should the linter only activate on an exact match
30 -- optional param, default: false (require exact match)
31 -- loffset: line offset will be added to the line number returned by the linter
32 -- useful if the linter returns 0-indexed lines
33 -- optional param, default: 0
34 -- coffset: column offset will be added to the col number returned by the linter
35 -- useful if the linter returns 0-indexed columns
36 -- optional param, default: 0
37 -- callback: function to call before executing the linter, if it returns
38 -- false the lint is canceled. The callback is passed the buf.
39 -- optional param, default: nil
40 function makeLinter(name, filetype, cmd, args, errorformat, os, whitelist, domatch, loffset, coffset, callback)
41 if linters[name] == nil then
43 linters[name].filetype = filetype
44 linters[name].cmd = cmd
45 linters[name].args = args
46 linters[name].errorformat = errorformat
47 linters[name].os = os or {}
48 linters[name].whitelist = whitelist or false
49 linters[name].domatch = domatch or false
50 linters[name].loffset = loffset or 0
51 linters[name].coffset = coffset or 0
52 linters[name].callback = callback or nil
56 function removeLinter(name)
61 local devnull = "/dev/null"
62 if runtime.GOOS == "windows" then
66 makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
67 makeLinter("gcc", "c++", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
68 makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m")
69 makeLinter("gobuild", "go", "go", {"build", "-o", devnull}, "%f:%l:%c:? %m")
70 -- makeLinter("golint", "go", "golint", {"%f"}, "%f:%l:%c: %m")
71 makeLinter("javac", "java", "javac", {"-d", "%d", "%f"}, "%f:%l: error: %m")
72 makeLinter("jshint", "javascript", "jshint", {"%f"}, "%f: line %l,.+, %m")
73 makeLinter("literate", "literate", "lit", {"-c", "%f"}, "%f:%l:%m", {}, false, true)
74 makeLinter("luacheck", "lua", "luacheck", {"--no-color", "%f"}, "%f:%l:%c: %m")
75 makeLinter("nim", "nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", "%f"}, "%f.%l, %c. %m")
76 makeLinter("clang", "objective-c", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
77 makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m")
78 makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m")
79 makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m")
80 makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m")
81 makeLinter("swiftc", "swift", "xcrun", {"swiftc", "%f"}, "%f:%l:%c:.+: %m", {"darwin"}, true)
82 makeLinter("swiftc", "swiftc", {"%f"}, "%f:%l:%c:.+: %m", {"linux"}, true)
83 makeLinter("yaml", "yaml", "yamllint", {"--format", "parsable", "%f"}, "%f:%l:%c:.+ %m")
85 config.MakeCommand("lint", "linter.lintCmd", config.NoComplete)
86 config.AddRuntimeFile("linter", config.RTHelp, "help/linter.md")
89 function lintCmd(bp, args)
94 function contains(list, element)
95 for k, v in pairs(list) do
103 function runLinter(buf)
104 local ft = buf:FileType()
105 local file = buf.Path
106 local dir = filepath.Dir(file)
108 for k, v in pairs(linters) do
109 local ftmatch = ft == v.filetype
111 ftmatch = string.match(ft, v.filetype)
114 local hasOS = contains(v.os, runtime.GOOS)
115 if not hasOS and v.whitelist then
118 if hasOS and not v.whitelist then
123 for k, arg in pairs(v.args) do
124 args[k] = arg:gsub("%%f", file):gsub("%%d", dir)
128 lint(buf, k, v.cmd, args, v.errorformat, v.loffset, v.coffset, v.callback)
138 function lint(buf, linter, cmd, args, errorformat, loff, coff, callback)
139 buf:ClearMessages(linter)
141 if callback ~= nil then
142 if not callback(buf) then
147 shell.JobSpawn(cmd, args, "", "", "linter.onExit", buf, linter, errorformat, loff, coff)
150 function onExit(output, args)
151 local buf, linter, errorformat, loff, coff = args[1], args[2], args[3], args[4], args[5]
152 local lines = split(output, "\n")
154 local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%c", "(%d+)"):gsub("%%m", "(.+)")
155 for _,line in ipairs(lines) do
157 line = line:match("^%s*(.+)%s*$")
158 if string.find(line, regex) then
159 local file, line, col, msg = string.match(line, regex)
161 if not string.find(errorformat, "%%c") then
164 elseif col == nil then
167 micro.Log(basename(buf.Path), basename(file))
168 if basename(buf.Path) == basename(file) then
171 local mstart = buffer.Loc(tonumber(col-1+coff), tonumber(line-1+loff))
172 local mend = buffer.Loc(tonumber(col+coff), tonumber(line-1+loff))
173 bmsg = buffer.NewMessage(linter, msg, mstart, mend, buffer.MTError)
175 bmsg = buffer.NewMessageAtLine(linter, msg, tonumber(line+loff), buffer.MTError)
183 function split(str, sep)
185 local regex = ("([^%s]+)"):format(sep)
186 for each in str:gmatch(regex) do
187 table.insert(result, each)
192 function basename(file)
194 if runtime.GOOS == "windows" then
197 local name = string.gsub(file, "(.*" .. sep .. ")(.*)", "%2")