|
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