File: linter.lua

package info (click to toggle)
micro 2.0.15-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,128 kB
  • sloc: sh: 265; makefile: 77; xml: 53
file content (223 lines) | stat: -rw-r--r-- 8,713 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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")
local util = import("micro/util")
local os = import("os")

local linters = {}

-- 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 removeLinter(name)
    linters[name] = nil
end

function preinit()
    local devnull = "/dev/null"
    if runtime.GOOS == "windows" then
        devnull = "NUL"
    end

    makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
    makeLinter("g++", "c++", "g++", {"-fsyntax-only","-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
    makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m")
    makeLinter("ldc2", "d", "ldc2", {"--o-", "--vcolumns", "-w", "-c", "%f"}, "%f%(%l,%c%):[^:]+: %m")
    makeLinter("gdc", "d", "gdc", {"-fsyntax-only","-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
    makeLinter("eslint", "javascript", "eslint", {"-f","compact","%f"}, "%f: line %l, col %c, %m")
    makeLinter("gobuild", "go", "go", {"build", "-o", devnull, "%d"}, "%f:%l:%c:? %m")
    makeLinter("govet", "go", "go", {"vet"}, "%f:%l:%c: %m")
    makeLinter("clippy", "rust", "cargo", {"clippy", "--message-format", "short"}, "%f:%l:%c: %m")
    makeLinter("hlint", "haskell", "hlint", {"%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("ruff", "python", "ruff", {"check", "--output-format=concise", "%f"}, "%f:%l:%c: %m")
    makeLinter("flake8", "python", "flake8", {"%f"}, "%f:%l:%c: %m")
    makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m")
    makeLinter("shellcheck", "shell", "shellcheck", {"-f", "gcc", "%f"}, "%f:%l:%c:.+: %m")
    makeLinter("swiftc", "swift", "xcrun", {"swiftc", "%f"}, "%f:%l:%c:.+: %m", {"darwin"}, true)
    makeLinter("swiftc-linux", "swift", "swiftc", {"%f"}, "%f:%l:%c:.+: %m", {"linux"}, true)
    makeLinter("yaml", "yaml", "yamllint", {"--format", "parsable", "%f"}, "%f:%l:%c:.+ %m")
    makeLinter("nix-linter", "nix", "nix-linter", {"%f"}, "%m at %f:%l:%c", {"linux"}, true)

    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 checkFtMatch(ft, v)
    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
    return ftmatch
end

function runLinter(buf)
    local ft = buf:FileType()
    local file = buf.Path
    local dir = "." .. util.RuneStr(os.PathSeparator) .. filepath.Dir(file)

    for k, v in pairs(linters) do
        if checkFtMatch(ft, v) then
            local args = {}
            for k, arg in pairs(v.args) do
                args[k] = arg:gsub("%%f", file):gsub("%%d", dir)
            end
            lint(buf, k, v.cmd, args, v.errorformat, v.loffset, v.coffset, v.callback)
        end
    end
end

function onSave(bp)
    runLinter(bp.Buf)
    return true
end

function onBufferOptionChanged(buf, option, old, new)
    if option == "filetype" then
        if old ~= new then
            for k, v in pairs(linters) do
                if checkFtMatch(old, v) then
                    buf:ClearMessages(k)
                end
            end
        end
    end
    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

    shell.JobSpawn(cmd, args, nil, nil, onExit, buf, linter, errorformat, loff, coff)
end

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("%%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, 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
            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
end

function split(str, sep)
    local result = {}
    local regex = ("([^%s]+)"):format(sep)
    for each in str:gmatch(regex) do
        table.insert(result, each)
    end
    return result
end

function basename(file)
    local sep = "/"
    if runtime.GOOS == "windows" then
        sep = "\\"
    end
    local name = string.gsub(file, "(.*" .. sep .. ")(.*)", "%2")
    return name
end