]> git.lizzy.rs Git - micro.git/blob - runtime/plugins/linter/linter.lua
Merge pull request #1437 from serebit/patch-2
[micro.git] / runtime / plugins / linter / linter.lua
1 local micro = import("micro")
2 local runtime = import("runtime")
3 local filepath = import("path/filepath")
4 local shell = import("micro/shell")
5 local buffer = import("micro/buffer")
6 local config = import("micro/config")
7
8 local linters = {}
9
10 -- creates a linter entry, call from within an initialization function, not
11 -- directly at initial load time
12 --
13 -- name: name of the linter
14 -- filetype: filetype to check for to use linter
15 -- cmd: main linter process that is executed
16 -- args: arguments to pass to the linter process
17 --     use %f to refer to the current file name
18 --     use %d to refer to the current directory name
19 -- errorformat: how to parse the linter/compiler process output
20 --     %f: file, %l: line number, %m: error/warning message
21 -- os: list of OSs this linter is supported or unsupported on
22 --     optional param, default: {}
23 -- whitelist: should the OS list be a blacklist (do not run the linter for these OSs)
24 --            or a whitelist (only run the linter for these OSs)
25 --     optional param, default: false (should blacklist)
26 -- domatch: should the filetype be interpreted as a lua pattern to match with
27 --          the actual filetype, or should the linter only activate on an exact match
28 --     optional param, default: false (require exact match)
29 -- loffset: line offset will be added to the line number returned by the linter
30 --          useful if the linter returns 0-indexed lines
31 --     optional param, default: 0
32 -- coffset: column offset will be added to the col number returned by the linter
33 --          useful if the linter returns 0-indexed columns
34 --     optional param, default: 0
35 function makeLinter(name, filetype, cmd, args, errorformat, os, whitelist, domatch, loffset, coffset)
36     if linters[name] == nil then
37         linters[name] = {}
38         linters[name].filetype = filetype
39         linters[name].cmd = cmd
40         linters[name].args = args
41         linters[name].errorformat = errorformat
42         linters[name].os = os or {}
43         linters[name].whitelist = whitelist or false
44         linters[name].domatch = domatch or false
45         linters[name].loffset = loffset or 0
46         linters[name].coffset = coffset or 0
47     end
48 end
49
50 function removeLinter(name)
51     linters[name] = nil
52 end
53
54 function init()
55     local devnull = "/dev/null"
56     if runtime.GOOS == "windows" then
57         devnull = "NUL"
58     end
59
60     makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
61     makeLinter("gcc", "c++", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
62     makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m")
63     makeLinter("gobuild", "go", "go", {"build", "-o", devnull}, "%f:%l:%c:? %m")
64     -- makeLinter("golint", "go", "golint", {"%f"}, "%f:%l:%c: %m")
65     makeLinter("javac", "java", "javac", {"-d", "%d", "%f"}, "%f:%l: error: %m")
66     makeLinter("jshint", "javascript", "jshint", {"%f"}, "%f: line %l,.+, %m")
67     makeLinter("literate", "literate", "lit", {"-c", "%f"}, "%f:%l:%m", {}, false, true)
68     makeLinter("luacheck", "lua", "luacheck", {"--no-color", "%f"}, "%f:%l:%c: %m")
69     makeLinter("nim", "nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", "%f"}, "%f.%l, %c. %m")
70     makeLinter("clang", "objective-c", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
71     makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m")
72     makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m")
73     makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m")
74     makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m")
75     makeLinter("swiftc", "swift", "xcrun", {"swiftc", "%f"}, "%f:%l:%c:.+: %m", {"darwin"}, true)
76     makeLinter("swiftc", "swiftc", {"%f"}, "%f:%l:%c:.+: %m", {"linux"}, true)
77     makeLinter("yaml", "yaml", "yamllint", {"--format", "parsable", "%f"}, "%f:%l:%c:.+ %m")
78
79     config.MakeCommand("lint", "linter.lintCmd", config.NoComplete)
80 end
81
82 function lintCmd(bp, args)
83     bp:Save()
84     runLinter(bp.Buf)
85 end
86
87 function contains(list, element)
88     for k, v in pairs(list) do
89         if v == element then
90             return true
91         end
92     end
93     return false
94 end
95
96 function runLinter(buf)
97     local ft = buf:FileType()
98     local file = buf.Path
99     local dir = filepath.Dir(file)
100
101     for k, v in pairs(linters) do
102         local ftmatch = ft == v.filetype
103         if v.domatch then
104             ftmatch = string.match(ft, v.filetype)
105         end
106
107         local hasOS = contains(v.os, runtime.GOOS)
108         if not hasOS and v.whitelist then
109             ftmatch = false
110         end
111         if hasOS and not v.whitelist then
112             ftmatch = false
113         end
114
115         local args = {}
116         for k, arg in pairs(v.args) do
117             args[k] = arg:gsub("%%f", file):gsub("%%d", dir)
118         end
119
120         if ftmatch then
121             lint(buf, k, v.cmd, args, v.errorformat, v.loffset, v.coffset)
122         end
123     end
124 end
125
126 function onSave(bp)
127     runLinter(bp.Buf)
128     return true
129 end
130
131 function lint(buf, linter, cmd, args, errorformat, loff, coff)
132     buf:ClearMessages(linter)
133
134     shell.JobSpawn(cmd, args, "", "", "linter.onExit", buf, linter, errorformat, loff, coff)
135 end
136
137 function onExit(output, args)
138     local buf, linter, errorformat, loff, coff = args[1], args[2], args[3], args[4], args[5]
139     local lines = split(output, "\n")
140
141     local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%c", "(%d+)"):gsub("%%m", "(.+)")
142     for _,line in ipairs(lines) do
143         -- Trim whitespace
144         line = line:match("^%s*(.+)%s*$")
145         if string.find(line, regex) then
146             local file, line, col, msg = string.match(line, regex)
147             local hascol = true
148             if not string.find(errorformat, "%%c") then
149                 hascol = false
150                 msg = col
151             elseif col == nil then
152                 hascol = false
153             end
154             micro.Log(basename(buf.Path), basename(file))
155             if basename(buf.Path) == basename(file) then
156                 local bmsg = nil
157                 if hascol then
158                     local mstart = buffer.Loc(tonumber(col-1+coff), tonumber(line-1+loff))
159                     local mend = buffer.Loc(tonumber(col+coff), tonumber(line-1+loff))
160                     bmsg = buffer.NewMessage(linter, msg, mstart, mend, buffer.MTError)
161                 else
162                     bmsg = buffer.NewMessageAtLine(linter, msg, tonumber(line+loff), buffer.MTError)
163                 end
164                 buf:AddMessage(bmsg)
165             end
166         end
167     end
168 end
169
170 function split(str, sep)
171     local result = {}
172     local regex = ("([^%s]+)"):format(sep)
173     for each in str:gmatch(regex) do
174         table.insert(result, each)
175     end
176     return result
177 end
178
179 function basename(file)
180     local sep = "/"
181     if runtime.GOOS == "windows" then
182         sep = "\\"
183     end
184     local name = string.gsub(file, "(.*" .. sep .. ")(.*)", "%2")
185     return name
186 end