Skip to content

Commit f8bb42b

Browse files
authored
Merge pull request #3 from nfnt/nfnt/simplify-linter
Add simpler linter integration with golangci-lint command
2 parents bb3544f + a0b0315 commit f8bb42b

File tree

2 files changed

+17
-279
lines changed

2 files changed

+17
-279
lines changed

README.md

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,7 @@ This linter plugin for [SublimeLinter](https://github.com/SublimeLinter) provide
1616

1717
In order for `golangci-lint` to be executed by SublimeLinter, you must ensure that its path is available to SublimeLinter. Before going any further, please read and follow the steps in [Finding a linter executable](http://sublimelinter.readthedocs.org/en/latest/troubleshooting.html#finding-a-linter-executable) through “Validating your PATH” in the documentation. Once you have installed `golangci-lint`, you can proceed to install the plugin if it is not yet installed.
1818

19-
Due to performance issues in golangci-lint, the linter will not attempt to lint more than one-hundred (100) files considering a delay of 100ms and `lint_mode` equal to “background”. If the user increases the delay, the tool will have more time to scan more files and analyze them. If your project contains more than 300 files, you’ll have to set a delay of 0.3s or more in SublimeLinter settings.
20-
21-
**Note:** The linter creates a temporary directory to allow SublimeLinter to scan changes in the code that are still in the buffer _(aka. not saved yet)_. If the SublimeText sidebar is visible, you will notice _—for a split of a second—_ that a folder named `.golangcilint-*` appears and disappears. Make sure to add this folder to your `.gitignore` file, and also the “folder_exclude_patterns” in SublimeText’s preferences:
22-
23-
```
24-
{
25-
"folder_exclude_patterns":
26-
[
27-
".svn",
28-
".git",
29-
".hg",
30-
"CVS",
31-
"cache",
32-
"uploads",
33-
".golangci-*",
34-
".golangcilint-*",
35-
".gometalinter-*"
36-
]
37-
}
38-
```
19+
Due to the way that golangci-lint works, the linter will only run when saving a file, even if `lint_mode` is set to “background”.
3920

4021
## Plugin installation
4122

linter.py

Lines changed: 16 additions & 259 deletions
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,16 @@
1-
import os
2-
import json
3-
import logging
4-
import tempfile
5-
from SublimeLinter.lint import NodeLinter, util
6-
from SublimeLinter.lint.linter import LintMatch
7-
from SublimeLinter.lint.persist import settings
8-
9-
logger = logging.getLogger('SublimeLinter.plugin.golangcilint')
10-
11-
12-
class Golangcilint(NodeLinter):
13-
# Here are the statistics of how fast the plugin reports the warnings and
14-
# errors via golangci-lint when all the helpers are disabled and only the
15-
# specified linter is enabled. In total, when all of them are enabled, it
16-
# takes an average of 1.6111 secs in a project with seventy-four (74) Go
17-
# files, 6043 lines (4620 code + 509 comments + 914 blanks).
18-
#
19-
# | Seconds | Linter |
20-
# |---------|-------------|
21-
# | 0.7040s | goconst |
22-
# | 0.7085s | nakedret |
23-
# | 0.7172s | gocyclo |
24-
# | 0.7337s | prealloc |
25-
# | 0.7431s | scopelint |
26-
# | 0.7479s | ineffassign |
27-
# | 0.7553s | golint |
28-
# | 0.7729s | misspell |
29-
# | 0.7733s | gofmt |
30-
# | 0.7854s | dupl |
31-
# | 1.2574s | varcheck |
32-
# | 1.2653s | errcheck |
33-
# | 1.3052s | gocritic |
34-
# | 1.3078s | typecheck |
35-
# | 1.3131s | structcheck |
36-
# | 1.3140s | maligned |
37-
# | 1.3159s | unconvert |
38-
# | 1.3598s | depguard |
39-
# | 1.3678s | deadcode |
40-
# | 1.3942s | govet |
41-
# | 1.4565s | gosec |
42-
cmd = "golangci-lint run --fast --out-format json"
43-
defaults = {"selector": "source.go"}
44-
error_stream = util.STREAM_BOTH
45-
line_col_base = (1, 1)
46-
47-
def run(self, cmd, code):
48-
if not os.path.dirname(self.filename):
49-
logger.warning("cannot lint unsaved Go (golang) files")
50-
self.notify_failure()
51-
return ""
52-
53-
# If the user has configured SublimeLinter to run in background mode,
54-
# the linter will be unable to show warnings or errors in the current
55-
# buffer until the user saves the changes. To solve this problem, the
56-
# plugin will create a temporary directory, then will create symbolic
57-
# links of all the files in the current folder, then will write the
58-
# buffer into a file, and finally will execute the linter inside this
59-
# directory.
60-
#
61-
# Note: The idea to execute the Foreground linter “on_load” even if
62-
# “lint_mode” is set to “background” cannot be taken in consideration
63-
# because of the following scenario:
64-
#
65-
# - User makes changes to a file
66-
# - The editor suddently closes
67-
# - Buffer is saved for recovery
68-
# - User opens the editor again
69-
# - Editor loads the unsaved file
70-
# - Linter runs in an unsaved file
71-
if settings.get("lint_mode") == "background":
72-
return self._background_lint(cmd, code)
73-
else:
74-
return self._foreground_lint(cmd)
75-
76-
"""match regex against the command output"""
77-
def find_errors(self, output):
78-
current = os.path.basename(self.filename)
79-
exclude = False
80-
81-
try:
82-
data = json.loads(output)
83-
except Exception as e:
84-
logger.warning(e)
85-
self.notify_failure()
86-
87-
"""merge possible stderr with issues"""
88-
if (data
89-
and "Report" in data
90-
and "Error" in data["Report"]):
91-
for line in data["Report"]["Error"].splitlines():
92-
if line.count(":") < 3:
93-
continue
94-
if line.startswith("typechecking error: "):
95-
line = line[20:]
96-
if line[1:3] == ":\\": # windows path in filename
97-
parts = line.split(":", 4)
98-
data["Issues"].append({
99-
"FromLinter": "typecheck",
100-
"Text": parts[4].strip(),
101-
"Pos": {
102-
"Filename": ':'.join(parts[0:2]),
103-
"Line": parts[2],
104-
"Column": parts[3],
105-
}
106-
})
107-
else:
108-
parts = line.split(":", 3)
109-
data["Issues"].append({
110-
"FromLinter": "typecheck",
111-
"Text": parts[3].strip(),
112-
"Pos": {
113-
"Filename": parts[0],
114-
"Line": parts[1],
115-
"Column": parts[2],
116-
}
117-
})
118-
119-
"""find relevant issues and yield a LintMatch"""
120-
if data and "Issues" in data:
121-
for issue in data["Issues"]:
122-
"""fix 3rd-party linter bugs"""
123-
issue = self._formalize(issue)
124-
125-
"""detect broken canonical imports"""
126-
if ("code in directory" in issue["Text"]
127-
and "expects import" in issue["Text"]):
128-
issue = self._canonical(issue)
129-
yield self._lintissue(issue)
130-
exclude = True
131-
continue
132-
133-
"""ignore false positive warnings"""
134-
if (exclude
135-
and "could not import" in issue["Text"]
136-
and "missing package:" in issue["Text"]):
137-
continue
138-
139-
"""issues found in the current file are relevant"""
140-
if self._shortname(issue) != current:
141-
continue
142-
143-
yield self._lintissue(issue)
144-
145-
def on_stderr(self, output):
146-
logger.warning('{} output:\n{}'.format(self.name, output))
147-
self.notify_failure()
148-
149-
def finalize_cmd(self, cmd, context, at_value='', auto_append=False):
150-
"""prevents SublimeLinter from appending an unnecessary file"""
151-
return cmd
152-
153-
def _foreground_lint(self, cmd):
154-
return self.communicate(cmd)
155-
156-
def _background_lint(self, cmd, code):
157-
folder = os.path.dirname(self.filename)
158-
things = [f for f in os.listdir(folder) if f.endswith(".go")]
159-
maxsee = settings.get("delay") * 1000
160-
nfiles = len(things)
161-
162-
if nfiles > maxsee:
163-
# Due to performance issues in golangci-lint, the linter will not
164-
# attempt to lint more than one-hundred (100) files considering a
165-
# delay of 100ms and lint_mode equal to “background”. If the user
166-
# increases the delay, the tool will have more time to scan more
167-
# files and analyze them.
168-
logger.warning("too many Go (golang) files ({})".format(nfiles))
169-
self.notify_failure()
170-
return ""
171-
172-
try:
173-
"""create temporary folder to store the code from the buffer"""
174-
with tempfile.TemporaryDirectory(dir=folder, prefix=".golangcilint-") as tmpdir:
175-
for filepath in things:
176-
target = os.path.join(tmpdir, filepath)
177-
filepath = os.path.join(folder, filepath)
178-
"""create symbolic links to non-modified files"""
179-
if os.path.basename(target) != os.path.basename(self.filename):
180-
os.link(filepath, target)
181-
continue
182-
"""write the buffer into a file on disk"""
183-
with open(target, 'wb') as w:
184-
if isinstance(code, str):
185-
code = code.encode('utf8')
186-
w.write(code)
187-
"""point command to the temporary folder"""
188-
return self.communicate(cmd + [tmpdir])
189-
except FileNotFoundError:
190-
logger.warning("cannot lint non-existent folder “{}”".format(folder))
191-
self.notify_failure()
192-
return ""
193-
except PermissionError:
194-
logger.warning("cannot lint private folder “{}”".format(folder))
195-
self.notify_failure()
196-
return ""
197-
198-
def _formalize(self, issue):
199-
"""some linters return numbers as string"""
200-
if not isinstance(issue["Pos"]["Line"], int):
201-
issue["Pos"]["Line"] = int(issue["Pos"]["Line"])
202-
if not isinstance(issue["Pos"]["Column"], int):
203-
issue["Pos"]["Column"] = int(issue["Pos"]["Column"])
204-
return issue
205-
206-
def _shortname(self, issue):
207-
"""find and return short filename"""
208-
return os.path.basename(issue["Pos"]["Filename"])
209-
210-
def _severity(self, issue):
211-
"""consider /dev/stderr as errors and /dev/stdout as warnings"""
212-
return "error" if issue["FromLinter"] == "typecheck" else "warning"
213-
214-
def _canonical(self, issue):
215-
mark = issue["Text"].rfind("/")
216-
package = issue["Text"][mark+1:-1]
217-
# Go 1.4 introduces an annotation for package clauses in Go source that
218-
# identify a canonical import path for the package. If an import is
219-
# attempted using a path that is not canonical, the go command will
220-
# refuse to compile the importing package.
221-
#
222-
# When the linter runs, it creates a temporary directory, for example,
223-
# “.golangcilint-foobar”, then creates a symbolic link for all relevant
224-
# files, and writes the content of the current buffer in the correct
225-
# file. Unfortunately, canonical imports break this flow because the
226-
# temporary directory differs from the expected location.
227-
#
228-
# The only way to deal with this for now is to detect the error, which
229-
# may as well be a false positive, and then ignore all the warnings
230-
# about missing packages in the current file. Hopefully, the user has
231-
# “goimports” which will automatically resolve the dependencies for
232-
# them. Also, if the false positives are not, the programmer will know
233-
# about the missing packages during the compilation phase, so it’s not
234-
# a bad idea to ignore these warnings for now.
235-
#
236-
# See: https://golang.org/doc/go1.4#canonicalimports
237-
return {
238-
"FromLinter": "typecheck",
239-
"Text": "cannot lint package “{}” due to canonical import path".format(package),
240-
"Replacement": issue["Replacement"],
241-
"SourceLines": issue["SourceLines"],
242-
"Level": "error",
243-
"Pos": {
244-
"Filename": self.filename,
245-
"Offset": 0,
246-
"Column": 0,
247-
"Line": 1
248-
}
249-
}
250-
251-
def _lintissue(self, issue):
252-
return LintMatch(
253-
match=issue,
254-
message=issue["Text"],
255-
error_type=self._severity(issue),
256-
line=issue["Pos"]["Line"] - self.line_col_base[0],
257-
col=issue["Pos"]["Column"] - self.line_col_base[1],
258-
code=issue["FromLinter"]
259-
)
1+
from SublimeLinter.lint import Linter, WARNING
2+
3+
4+
class GolangCILint(Linter):
5+
cmd = 'golangci-lint run --fast --out-format tab ${file_path}'
6+
tempfile_suffix = '-'
7+
# Column reporting is optional and not provided by all linters.
8+
# Issues reported by the 'typecheck' linter are treated as errors,
9+
# because they indicate code that won't compile. All other linter issues
10+
# are treated as warnings.
11+
regex = r'^(?P<filename>(\w+:\\\\)?.[^:]+):(?P<line>\d+)(:(?P<col>\d+))?\s+' + \
12+
r'(?P<code>(?P<error>typecheck)|\w+)\s+(?P<message>.+)$'
13+
default_type = WARNING
14+
defaults = {
15+
'selector': 'source.go'
16+
}

0 commit comments

Comments
 (0)