Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 1 addition & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,7 @@ This linter plugin for [SublimeLinter](https://github.com/SublimeLinter) provide

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.

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.

**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:

```
{
"folder_exclude_patterns":
[
".svn",
".git",
".hg",
"CVS",
"cache",
"uploads",
".golangci-*",
".golangcilint-*",
".gometalinter-*"
]
}
```
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”.

## Plugin installation

Expand Down
275 changes: 16 additions & 259 deletions linter.py
Original file line number Diff line number Diff line change
@@ -1,259 +1,16 @@
import os
import json
import logging
import tempfile
from SublimeLinter.lint import NodeLinter, util
from SublimeLinter.lint.linter import LintMatch
from SublimeLinter.lint.persist import settings

logger = logging.getLogger('SublimeLinter.plugin.golangcilint')


class Golangcilint(NodeLinter):
# Here are the statistics of how fast the plugin reports the warnings and
# errors via golangci-lint when all the helpers are disabled and only the
# specified linter is enabled. In total, when all of them are enabled, it
# takes an average of 1.6111 secs in a project with seventy-four (74) Go
# files, 6043 lines (4620 code + 509 comments + 914 blanks).
#
# | Seconds | Linter |
# |---------|-------------|
# | 0.7040s | goconst |
# | 0.7085s | nakedret |
# | 0.7172s | gocyclo |
# | 0.7337s | prealloc |
# | 0.7431s | scopelint |
# | 0.7479s | ineffassign |
# | 0.7553s | golint |
# | 0.7729s | misspell |
# | 0.7733s | gofmt |
# | 0.7854s | dupl |
# | 1.2574s | varcheck |
# | 1.2653s | errcheck |
# | 1.3052s | gocritic |
# | 1.3078s | typecheck |
# | 1.3131s | structcheck |
# | 1.3140s | maligned |
# | 1.3159s | unconvert |
# | 1.3598s | depguard |
# | 1.3678s | deadcode |
# | 1.3942s | govet |
# | 1.4565s | gosec |
cmd = "golangci-lint run --fast --out-format json"
defaults = {"selector": "source.go"}
error_stream = util.STREAM_BOTH
line_col_base = (1, 1)

def run(self, cmd, code):
if not os.path.dirname(self.filename):
logger.warning("cannot lint unsaved Go (golang) files")
self.notify_failure()
return ""

# If the user has configured SublimeLinter to run in background mode,
# the linter will be unable to show warnings or errors in the current
# buffer until the user saves the changes. To solve this problem, the
# plugin will create a temporary directory, then will create symbolic
# links of all the files in the current folder, then will write the
# buffer into a file, and finally will execute the linter inside this
# directory.
#
# Note: The idea to execute the Foreground linter “on_load” even if
# “lint_mode” is set to “background” cannot be taken in consideration
# because of the following scenario:
#
# - User makes changes to a file
# - The editor suddently closes
# - Buffer is saved for recovery
# - User opens the editor again
# - Editor loads the unsaved file
# - Linter runs in an unsaved file
if settings.get("lint_mode") == "background":
return self._background_lint(cmd, code)
else:
return self._foreground_lint(cmd)

"""match regex against the command output"""
def find_errors(self, output):
current = os.path.basename(self.filename)
exclude = False

try:
data = json.loads(output)
except Exception as e:
logger.warning(e)
self.notify_failure()

"""merge possible stderr with issues"""
if (data
and "Report" in data
and "Error" in data["Report"]):
for line in data["Report"]["Error"].splitlines():
if line.count(":") < 3:
continue
if line.startswith("typechecking error: "):
line = line[20:]
if line[1:3] == ":\\": # windows path in filename
parts = line.split(":", 4)
data["Issues"].append({
"FromLinter": "typecheck",
"Text": parts[4].strip(),
"Pos": {
"Filename": ':'.join(parts[0:2]),
"Line": parts[2],
"Column": parts[3],
}
})
else:
parts = line.split(":", 3)
data["Issues"].append({
"FromLinter": "typecheck",
"Text": parts[3].strip(),
"Pos": {
"Filename": parts[0],
"Line": parts[1],
"Column": parts[2],
}
})

"""find relevant issues and yield a LintMatch"""
if data and "Issues" in data:
for issue in data["Issues"]:
"""fix 3rd-party linter bugs"""
issue = self._formalize(issue)

"""detect broken canonical imports"""
if ("code in directory" in issue["Text"]
and "expects import" in issue["Text"]):
issue = self._canonical(issue)
yield self._lintissue(issue)
exclude = True
continue

"""ignore false positive warnings"""
if (exclude
and "could not import" in issue["Text"]
and "missing package:" in issue["Text"]):
continue

"""issues found in the current file are relevant"""
if self._shortname(issue) != current:
continue

yield self._lintissue(issue)

def on_stderr(self, output):
logger.warning('{} output:\n{}'.format(self.name, output))
self.notify_failure()

def finalize_cmd(self, cmd, context, at_value='', auto_append=False):
"""prevents SublimeLinter from appending an unnecessary file"""
return cmd

def _foreground_lint(self, cmd):
return self.communicate(cmd)

def _background_lint(self, cmd, code):
folder = os.path.dirname(self.filename)
things = [f for f in os.listdir(folder) if f.endswith(".go")]
maxsee = settings.get("delay") * 1000
nfiles = len(things)

if nfiles > maxsee:
# 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.
logger.warning("too many Go (golang) files ({})".format(nfiles))
self.notify_failure()
return ""

try:
"""create temporary folder to store the code from the buffer"""
with tempfile.TemporaryDirectory(dir=folder, prefix=".golangcilint-") as tmpdir:
for filepath in things:
target = os.path.join(tmpdir, filepath)
filepath = os.path.join(folder, filepath)
"""create symbolic links to non-modified files"""
if os.path.basename(target) != os.path.basename(self.filename):
os.link(filepath, target)
continue
"""write the buffer into a file on disk"""
with open(target, 'wb') as w:
if isinstance(code, str):
code = code.encode('utf8')
w.write(code)
"""point command to the temporary folder"""
return self.communicate(cmd + [tmpdir])
except FileNotFoundError:
logger.warning("cannot lint non-existent folder “{}”".format(folder))
self.notify_failure()
return ""
except PermissionError:
logger.warning("cannot lint private folder “{}”".format(folder))
self.notify_failure()
return ""

def _formalize(self, issue):
"""some linters return numbers as string"""
if not isinstance(issue["Pos"]["Line"], int):
issue["Pos"]["Line"] = int(issue["Pos"]["Line"])
if not isinstance(issue["Pos"]["Column"], int):
issue["Pos"]["Column"] = int(issue["Pos"]["Column"])
return issue

def _shortname(self, issue):
"""find and return short filename"""
return os.path.basename(issue["Pos"]["Filename"])

def _severity(self, issue):
"""consider /dev/stderr as errors and /dev/stdout as warnings"""
return "error" if issue["FromLinter"] == "typecheck" else "warning"

def _canonical(self, issue):
mark = issue["Text"].rfind("/")
package = issue["Text"][mark+1:-1]
# Go 1.4 introduces an annotation for package clauses in Go source that
# identify a canonical import path for the package. If an import is
# attempted using a path that is not canonical, the go command will
# refuse to compile the importing package.
#
# When the linter runs, it creates a temporary directory, for example,
# “.golangcilint-foobar”, then creates a symbolic link for all relevant
# files, and writes the content of the current buffer in the correct
# file. Unfortunately, canonical imports break this flow because the
# temporary directory differs from the expected location.
#
# The only way to deal with this for now is to detect the error, which
# may as well be a false positive, and then ignore all the warnings
# about missing packages in the current file. Hopefully, the user has
# “goimports” which will automatically resolve the dependencies for
# them. Also, if the false positives are not, the programmer will know
# about the missing packages during the compilation phase, so it’s not
# a bad idea to ignore these warnings for now.
#
# See: https://golang.org/doc/go1.4#canonicalimports
return {
"FromLinter": "typecheck",
"Text": "cannot lint package “{}” due to canonical import path".format(package),
"Replacement": issue["Replacement"],
"SourceLines": issue["SourceLines"],
"Level": "error",
"Pos": {
"Filename": self.filename,
"Offset": 0,
"Column": 0,
"Line": 1
}
}

def _lintissue(self, issue):
return LintMatch(
match=issue,
message=issue["Text"],
error_type=self._severity(issue),
line=issue["Pos"]["Line"] - self.line_col_base[0],
col=issue["Pos"]["Column"] - self.line_col_base[1],
code=issue["FromLinter"]
)
from SublimeLinter.lint import Linter, WARNING


class GolangCILint(Linter):
cmd = 'golangci-lint run --fast --out-format tab ${file_path}'
tempfile_suffix = '-'
# Column reporting is optional and not provided by all linters.
# Issues reported by the 'typecheck' linter are treated as errors,
# because they indicate code that won't compile. All other linter issues
# are treated as warnings.
regex = r'^(?P<filename>(\w+:\\\\)?.[^:]+):(?P<line>\d+)(:(?P<col>\d+))?\s+' + \
r'(?P<code>(?P<error>typecheck)|\w+)\s+(?P<message>.+)$'
default_type = WARNING
defaults = {
'selector': 'source.go'
}