-
-
Notifications
You must be signed in to change notification settings - Fork 272
/
linter.py
1875 lines (1542 loc) · 66.4 KB
/
linter.py
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from bisect import bisect_right
from collections import ChainMap, Mapping, Sequence
from contextlib import contextmanager
from fnmatch import fnmatch
from functools import lru_cache
import inspect
from itertools import chain
import logging
import os
import re
import shlex
import subprocess
import sys
import tempfile
import sublime
from . import persist, util
from .const import WARNING, ERROR
MYPY = False
if MYPY:
from typing import (
Any, Callable, Dict, List, IO, Iterator, Match, MutableMapping,
Optional, Pattern, Tuple, Type, Union
)
from .persist import LintError
Reason = str
logger = logging.getLogger(__name__)
ARG_RE = re.compile(r'(?P<prefix>@|--?)?(?P<name>[@\w][\w\-]*)(?:(?P<joiner>[=:])?(?:(?P<sep>.)(?P<multiple>\+)?)?)?')
BASE_CLASSES = ('PythonLinter', 'RubyLinter', 'NodeLinter', 'ComposerLinter')
# Many linters use stdin, and we convert text to utf-8
# before sending to stdin, so we have to make sure stdin
# in the target executable is looking for utf-8. Some
# linters (like ruby) need to have LANG and/or LC_CTYPE
# set as well.
UTF8_ENV_VARS = {
'PYTHONIOENCODING': 'utf8',
'LANG': 'en_US.UTF-8',
'LC_CTYPE': 'en_US.UTF-8',
}
BASE_LINT_ENVIRONMENT = ChainMap(UTF8_ENV_VARS, os.environ)
# ACCEPTED_REASONS_PER_MODE defines a list of acceptable reasons
# for each lint_mode. It aims to provide a better visibility to
# how lint_mode is implemented. The map is supposed to be used in
# this module only.
ACCEPTED_REASONS_PER_MODE = {
"manual": ("on_user_request",),
"save": ("on_user_request", "on_save"),
"load_save": ("on_user_request", "on_save", "on_load"),
"background": ("on_user_request", "on_save", "on_load", "on_modified"),
} # type: Dict[str, Tuple[str, ...]]
KNOWN_REASONS = set(chain(*ACCEPTED_REASONS_PER_MODE.values()))
LEGACY_LINT_MATCH_DEF = ("match", "line", "col", "error", "warning", "message", "near")
COMMON_CAPTURING_NAMES = (
"filename", "error_type", "code", "end_line", "end_col"
) + LEGACY_LINT_MATCH_DEF
class LintMatch(dict):
"""Convenience dict-a-like type representing Lint errors.
Historically, lint errors were tuples, and later namedtuples. This dict
class implements enough to be backwards compatible to a namedtuple as a
`LEGACY_LINT_MATCH_DEF` set.
Some convenience for the user: All present keys can be accessed like an
attribute. All commonly used names (see: COMMON_CAPTURING_NAMES) can
be safely accessed like an attribute, returning `None` if not present.
E.g.
error = LintMatch({'foo': 'bar'})
error.foo # 'bar'
error.error_type # None
error.quux # raises AttributeError
"""
if MYPY:
match = None # type: Optional[object]
filename = None # type: Optional[str]
line = None # type: int
col = None # type: Optional[int]
end_line = None # type: Optional[int]
end_col = None # type: Optional[int]
error_type = None # type: Optional[str]
code = None # type: Optional[str]
message = None # type: str
error = None # type: Optional[str]
warning = None # type: Optional[str]
near = None # type: Optional[str]
def __init__(self, *args, **kwargs):
if len(args) == 7:
self.update(zip(LEGACY_LINT_MATCH_DEF, args))
else:
super().__init__(*args, **kwargs)
def _replace(self, **kwargs):
self.update(kwargs)
return self
def __getattr__(self, name):
if name in COMMON_CAPTURING_NAMES:
return self.get(name, '' if name == 'message' else None)
try:
return self[name]
except KeyError:
raise AttributeError(
"'{}' object has no attribute '{}'".format(type(self).__name__, name)
) from None
def __getitem__(self, name):
if isinstance(name, int):
return tuple(iter(self))[name]
return super().__getitem__(name)
def __iter__(self):
return iter(tuple(getattr(self, name) for name in LEGACY_LINT_MATCH_DEF))
def copy(self):
return type(self)(self)
def __repr__(self):
return "{}({})".format(type(self).__name__, super().__repr__())
class TransientError(Exception):
...
class PermanentError(Exception):
...
# SublimeLinter can lint partial buffers, e.g. `<script>` tags inside a
# HTML-file. The tiny `VirtualView` is just enough code, so we can get the
# source code of a line, the linter reported to be problematic.
class VirtualView:
def __init__(self, code=''):
self._code = code
self._newlines = newlines = [0]
last = -1
while True:
last = code.find('\n', last + 1)
if last == -1:
break
newlines.append(last + 1)
newlines.append(len(code))
def full_line(self, line):
# type: (int) -> Tuple[int, int]
"""Return the start/end character positions for the given line."""
start = self._newlines[line]
end = self._newlines[min(line + 1, len(self._newlines) - 1)]
return start, end
def full_line_region(self, line):
# type: (int) -> sublime.Region
"""Return the (full) line region including any trailing newline char."""
return sublime.Region(*self.full_line(line))
def line_region(self, line):
# type: (int) -> sublime.Region
"""Return the line region without the possible trailing newline char."""
r = self.full_line_region(line)
t = self.substr(r).rstrip('\n')
return sublime.Region(r.a, r.a + len(t))
def select_line(self, line):
# type: (int) -> str
"""Return code for the given line."""
start, end = self.full_line(line)
return self._code[start:end]
def max_lines(self):
# type: () -> int
return len(self._newlines) - 2
def size(self):
# type: () -> int
return len(self._code)
def substr(self, region):
# type: (sublime.Region) -> str
return self._code[region.begin():region.end()]
def rowcol(self, offset):
# type: (int) -> Tuple[int, int]
"""Return the 0-based row and column for a given character offset"""
row = bisect_right(self._newlines, offset) - 1
return row, offset - self._newlines[row]
# Actual Sublime API would look like:
# def full_line(self, region)
# def full_line(self, point) => Region
# def substr(self, region)
# def text_point(self, row, col) => Point
# def rowcol(self, point) => (row, col)
@staticmethod
def from_file(filename):
# type: (str) -> VirtualView
"""Return a VirtualView with the contents of file."""
return _virtual_view_from_file(filename, os.path.getmtime(filename))
@lru_cache(maxsize=128)
def _virtual_view_from_file(filename, mtime):
# type: (str, float) -> VirtualView
with open(filename, 'r', encoding='utf8') as f:
return VirtualView(f.read())
class ViewSettings:
"""
Small wrapper around Sublime's view settings so we can use it in a
ChainMap.
In the standard Sublime settings system we store flattened objects.
So what is `{SublimeLinter: {linters: {flake8: args}}}` for the global
settings, becomes 'SublimeLinter.linters.flake8.args'
"""
# We need to use a str as marker bc the value gets *serialized* during
# roundtripping the Sublime API. A normal sentinel obj like `{}` would
# loose its identity.
NOT_PRESENT = '__NOT_PRESENT_MARKER__'
def __init__(self, view, prefix):
self.view = view
self.prefix = prefix
def _compute_final_key(self, key):
return self.prefix + key
def __getitem__(self, key):
value = self.view.settings().get(
self._compute_final_key(key), self.NOT_PRESENT)
if value == self.NOT_PRESENT: # must use '==' (!) see above
raise KeyError(key)
return value
def __contains__(self, key):
return self.view.settings().has(self._compute_final_key(key))
def __repr__(self):
return "ViewSettings({}, {!r})".format(
self.view.id(), self.prefix.rstrip('.'))
NOT_EXPANDABLE_SETTINGS = {
"lint_mode",
"selector",
"disable",
"filter_errors",
}
class LinterSettings:
"""
Smallest possible dict-like container for linter settings to lazy
substitute/expand variables found in the settings
"""
def __init__(self, raw_settings, context, _computed_settings=None):
# type: (Mapping[str, Any], Mapping[str, str], MutableMapping[str, Any]) -> None
self.raw_settings = raw_settings
self.context = context
self._computed_settings = {} if _computed_settings is None else _computed_settings
def __getitem__(self, key):
# type: (str) -> Any
if key in NOT_EXPANDABLE_SETTINGS:
return self.raw_settings[key]
try:
return self._computed_settings[key]
except KeyError:
try:
value = self.raw_settings[key]
except KeyError:
raise KeyError(key)
else:
final_value = substitute_variables(self.context, value)
self._computed_settings[key] = final_value
return final_value
def get(self, key, default=None):
# type: (str, Any) -> Any
return self[key] if key in self else default
def __contains__(self, key):
# type: (str) -> bool
return key in self._computed_settings or key in self.raw_settings
def __setitem__(self, key, value):
# type: (str, Any) -> None
self._computed_settings[key] = value
has = __contains__
set = __setitem__
def clone(self):
# type: () -> LinterSettings
return self.__class__(
self.raw_settings,
# Dirt-alert: We clone here bc we extract this context-object
# in `Linter.__init__`. In the scope of a linter instance,
# `self.context == self.settings.context` must hold.
ChainMap({}, self.context), # type: ignore[arg-type]
ChainMap({}, self._computed_settings)
)
def substitute_variables(variables, value):
# type: (Mapping, Any) -> Any
# Utilizes Sublime Text's `expand_variables` API, which uses the
# `${varname}` syntax and supports placeholders (`${varname:placeholder}`).
if isinstance(value, str):
# Workaround https://github.com/SublimeTextIssues/Core/issues/1878
# (E.g. UNC paths on Windows start with double slashes.)
value = value.replace(r'\\', r'\\\\')
value = sublime.expand_variables(value, variables)
return os.path.expanduser(value)
elif isinstance(value, Mapping):
return {key: substitute_variables(variables, val)
for key, val in value.items()}
elif isinstance(value, Sequence):
return [substitute_variables(variables, item)
for item in value]
else:
return value
def get_linter_settings(linter, view, context=None):
# type: (Type[Linter], sublime.View, Optional[Mapping[str, str]]) -> LinterSettings
"""Return 'final' linter settings with all variables expanded."""
if context is None:
context = get_view_context(view)
else:
context = ChainMap({}, context) # type: ignore[arg-type]
settings = get_raw_linter_settings(linter, view)
return LinterSettings(settings, context)
def get_raw_linter_settings(linter, view):
# type: (Type[Linter], sublime.View) -> MutableMapping[str, Any]
"""Return 'raw' linter settings without variables substituted."""
defaults = linter.defaults or {}
global_settings = persist.settings.get('linters', {}).get(linter.name, {})
view_settings = ViewSettings(
view, 'SublimeLinter.linters.{}.'.format(linter.name)
) # type: Mapping[str, Any] # type: ignore
return ChainMap(
{},
view_settings, # type: ignore[arg-type]
global_settings,
defaults,
{'lint_mode': persist.settings.get('lint_mode')}
)
def _extract_window_variables(window):
# type: (sublime.Window) -> Dict[str, str]
# We explicitly want to compute all variables around the current file
# on our own.
variables = window.extract_variables()
for key in (
'file', 'file_path', 'file_name', 'file_base_name', 'file_extension'
):
variables.pop(key, None)
return variables
def get_view_context(view, additional_context=None):
# type: (sublime.View, Optional[Mapping]) -> MutableMapping[str, str]
# Note that we ship a enhanced version for 'folder' if you have multiple
# folders open in a window. See `guess_project_root_of_view`.
window = view.window()
context = ChainMap(
{}, _extract_window_variables(window) if window else {}, os.environ
) # type: MutableMapping[str, str]
project_folder = guess_project_root_of_view(view)
if project_folder:
context['folder'] = project_folder
# `window.extract_variables` actually resembles data from the
# `active_view`, so we need to pass in all the relevant data around
# the filename manually in case the user switches to a different
# view, before we're done here.
filename = view.file_name()
if filename:
basename = os.path.basename(filename)
file_base_name, file_extension = os.path.splitext(basename)
context['file'] = filename
context['file_path'] = os.path.dirname(filename)
context['file_name'] = basename
context['file_base_name'] = file_base_name
context['file_extension'] = file_extension
context['canonical_filename'] = util.canonical_filename(view)
if additional_context:
context.update(additional_context)
return context
def guess_project_root_of_view(view):
window = view.window()
if not window:
return None
folders = window.folders()
if not folders:
return None
filename = view.file_name()
if not filename:
return folders[0]
for folder in folders:
# Take the first one; should we take the deepest one? The shortest?
if os.path.commonprefix([folder, filename]) == folder:
return folder
return None
class LinterMeta(type):
"""Metaclass for Linter and its subclasses."""
def __init__(cls, cls_name, bases, attrs): # type: ignore[misc]
# type: (Type[Linter], str, Tuple[object, ...], Dict[str, object]) -> None
"""
Initialize a Linter class.
When a Linter subclass is loaded by Sublime Text, this method is called.
We take this opportunity to do some transformations:
- Compile regex patterns.
- Convert strings to tuples where necessary.
- Build a map between defaults and linter arguments.
Finally, the class is registered as a linter for its configured syntax.
"""
if not bases:
return
if cls_name in BASE_CLASSES:
return
name = attrs.get('name') or cls_name.lower() # type: str # type: ignore[assignment]
cls.disabled = None
cls.name = name
cls.plugin_name = cls.__module__.split(".", 1)[0]
cls.logger = logging.getLogger('SublimeLinter.plugin.{}'.format(name))
# BEGIN DEPRECATIONS
for key in ('syntax', 'selectors'):
if key in attrs:
logger.error(
"{}: Defining 'cls.{}' has no effect anymore. Use "
"http://www.sublimelinter.com/en/stable/linter_settings.html#selector "
"instead."
.format(name, key)
)
cls.disabled = True
for key in (
'version_args', 'version_re', 'version_requirement',
'inline_settings', 'inline_overrides',
'comment_re', 'shebang_match',
'npm_name', 'composer_name',
'executable', 'executable_path',
'tab_width', 'config_file'
):
if key in attrs:
logger.warning(
"{}: Defining 'cls.{}' has no effect. Please cleanup and "
"remove this setting.".format(name, key))
for key in ('build_cmd', 'insert_args'):
if key in attrs:
logger.warning(
"{}: Do not implement '{}'. SublimeLinter will "
"change here in the near future.".format(name, key))
for key in ('can_lint', 'can_lint_syntax'):
if key in attrs:
logger.warning(
"{}: Implementing 'cls.{}' has no effect anymore. You "
"can safely remove this method.".format(name, key))
if (
'should_lint' in attrs
and not isinstance(attrs['should_lint'], classmethod)
):
logger.error(
"{} disabled. 'should_lint' now is a `@classmethod` and has a "
"different call signature. \nYou need to adapt the plugin code "
"because as it is the linter cannot run and thus will be "
"disabled. :-( \n\n"
"(Extending 'should_lint' is an edge-case and you probably don't "
"even need it, but if you do, look it up \n"
"in the source code on GitHub.)"
.format(name))
cls.disabled = True
if (
'get_environment' in attrs
and not len(inspect.getfullargspec(attrs['get_environment']).args) == 1
):
logger.error(
"{} disabled. 'get_environment' now has a simplified signature:\n"
" def get_environment(self): ...\n"
"The settings object can be retrieved via `self.settings`.\n"
"You need to update the linter plugin because as it is the "
"linter cannot run and thus will be disabled. :-("
.format(name))
cls.disabled = True
if (
'get_working_dir' in attrs
and not len(inspect.getfullargspec(attrs['get_working_dir']).args) == 1
):
logger.error(
"{} disabled. 'get_working_dir' now has a simplified signature:\n"
" def get_working_dir(self): ...\n"
"The settings object can be retrieved via `self.settings`.\n"
"You need to update the linter plugin because as it is the "
"linter cannot run and thus will be disabled. :-("
.format(name))
cls.disabled = True
# END DEPRECATIONS
# BEGIN CLASS MUTATIONS
cmd = attrs.get('cmd')
if isinstance(cmd, str):
cls.cmd = shlex.split(cmd)
if attrs.get('multiline', False):
cls.re_flags |= re.MULTILINE
for attr_name in ('regex', 'word_re'):
regex = attrs.get(attr_name)
if isinstance(regex, str):
try:
compiled_regex = re.compile(regex, cls.re_flags)
setattr(cls, attr_name, compiled_regex)
except re.error as err:
logger.error(
'{} disabled, error compiling {}: {}.'
.format(name, attr_name, str(err))
)
cls.disabled = True
else:
if attr_name == 'regex' and compiled_regex.flags & re.M == re.M:
cls.multiline = True
# If this class has its own defaults, create an args_map.
defaults = attrs.get('defaults', None)
if defaults and isinstance(defaults, dict):
cls.map_args(defaults)
# END CLASS MUTATIONS
# BEGIN VALIDATION
if not cls.cmd and cls.cmd is not None:
logger.error(
"{} disabled, 'cmd' must be specified."
.format(name)
)
cls.disabled = True
if not isinstance(cls.defaults, dict):
logger.error( # type: ignore[unreachable] # in case a user overrides our default
"{} disabled. 'cls.defaults' is mandatory and MUST be a dict."
.format(name)
)
cls.disabled = True
elif 'selector' not in cls.defaults:
if 'defaults' not in attrs:
logger.error(
"{} disabled. 'cls.defaults' is mandatory and MUST be a dict."
.format(name)
)
else:
logger.error(
"{} disabled. 'selector' is mandatory in 'cls.defaults'.\n See "
"http://www.sublimelinter.com/en/stable/linter_settings.html#selector"
.format(name))
cls.disabled = True
# END VALIDATION
if cls.disabled:
return
register_linter(name, cls)
def map_args(cls, defaults): # type: ignore[misc]
# type: (Type[Linter], Dict[str, object]) -> None
"""
Map plain setting names to args that will be passed to the linter executable.
For each item in defaults, the key is matched with ARG_RE. If there is a match,
the key is stripped of meta information and the match groups are stored as a dict
under the stripped key.
"""
# Check if the settings specify an argument.
# If so, add a mapping between the setting and the argument format,
# then change the name in the defaults to the setting name.
args_map = {}
cls.defaults = {}
for name, value in defaults.items():
match = ARG_RE.match(name)
if match and match.group('prefix'):
name = match.group('name')
args_map[name] = match.groupdict()
cls.defaults[name] = value
cls.args_map = args_map
def register_linter(name, cls):
# type: (str, Type[Linter]) -> None
"""Add a linter class to our mapping of class names <-> linter classes."""
persist.linter_classes[name] = cls
# Trigger a re-lint if SublimeLinter is already up and running. On Sublime
# start, this is generally not necessary, because SL will trigger various
# synthetic `on_activated_async` events on load.
if persist.api_ready:
deprecation_warning.cache_clear()
sublime.run_command('sublime_linter_config_changed')
logger.info('{} linter reloaded'.format(name))
@lru_cache(4)
def deprecation_warning(msg):
logger.warning(msg)
class Linter(metaclass=LinterMeta):
"""
The base class for linters.
Subclasses must at a minimum define the attributes syntax, cmd, and regex.
"""
#
# Public attributes
#
name = ''
logger = None # type: logging.Logger
if MYPY:
_CmdDefinition = Union[str, List[str], Tuple[str, ...]]
# A string, list, tuple or callable that returns a string, list or tuple, containing the
# command line (with arguments) used to lint.
cmd = '' # type: Union[None, _CmdDefinition, Callable[[], _CmdDefinition]]
# A regex pattern used to extract information from the executable's output.
regex = None # type: Union[None, str, Pattern]
# Set to True if the linter outputs multiline error messages. When True,
# regex will be created with the re.MULTILINE flag. If instead, you set
# the re.MULTILINE flag within the regex yourself, we in turn set this attribute
# to True automatically.
multiline = False
# If you want to set flags on the regex *other* than re.MULTILINE, set this.
re_flags = 0
# The default type assigned to non-classified errors. Should be either
# ERROR or WARNING.
default_type = ERROR
# Linters usually report errors with a line number, some with a column number
# as well. In general, most linters report one-based line numbers and column
# numbers. If a linter uses zero-based line numbers or column numbers, the
# linter class should define this attribute accordingly.
line_col_base = (1, 1)
# If the linter executable cannot receive from stdin and requires a temp file,
# set this attribute to the suffix of the temp file (with or without leading '.').
# If the suffix needs to be mapped to the syntax of a file, you may make this
# a dict that maps syntax names (all lowercase, as used in the syntax attribute),
# to tempfile suffixes. The syntax used to lookup the suffix is the mapped
# syntax, after using "syntax_map" in settings. If the view's syntax is not
# in this map, the class' syntax will be used.
#
# Some linters can only work from an actual disk file, because they
# rely on an entire directory structure that cannot be realistically be copied
# to a temp directory (e.g. javac). In such cases, set this attribute to '-',
# which marks the linter as "file-only". That will disable the linter for
# any views that are dirty.
tempfile_suffix = None # type: Union[None, str, Dict[str, str]]
# Linters may output to both stdout and stderr. By default stdout and sterr are captured.
# If a linter will never output anything useful on a stream (including when
# there is an error within the linter), you can ignore that stream by setting
# this attribute to the other stream.
error_stream = util.STREAM_BOTH
# If a linter reports a column position, SublimeLinter highlights the nearest
# word at that point. You can customize the regex used to highlight words
# by setting this to a pattern string or a compiled regex.
word_re = re.compile(r'^([-\w]+)')
# If you want to provide default settings for the linter, set this attribute.
# If a setting will be passed as an argument to the linter executable,
# you may specify the format of the argument here and the setting will
# automatically be passed as an argument to the executable. The format
# specification is as follows:
#
# <prefix><name><joiner>[<sep>[+]]
#
# - <prefix>: Either empty, '@', '-' or '--'.
# - <name>: The name of the setting.
# - <joiner>: Either '=' or ':'. If <prefix> is empty or '@', <joiner> is ignored.
# Otherwise, if '=', the setting value is joined with <name> by '=' and
# passed as a single argument. If ':', <name> and the value are passed
# as separate arguments.
# - <sep>: If the argument accepts a list of values, <sep> specifies
# the character used to delimit the list (usually ',').
# - +: If the setting can be a list of values, but each value must be
# passed as a separate argument, terminate the setting with '+'.
#
# After the format is parsed, the prefix and suffix are removed and the
# setting is replaced with <name>.
defaults = {} # type: Dict[str, Any]
# `disabled` has three states (None, True, False). It takes precedence
# over all other user or project settings.
disabled = None # type: Union[None, bool]
def __init__(self, view, settings):
# type: (sublime.View, LinterSettings) -> None
self.view = view
self.settings = settings
# Simplify tests which often just pass in a dict instead of
# real `LinterSettings`.
self.context = getattr(settings, 'context', {}) # type: MutableMapping[str, str]
# Using `self.env` is deprecated, bc it can have surprising
# side-effects for concurrent/async linting. We initialize it here
# bc some ruby linters rely on that behavior.
self.env = {} # type: Dict[str, str]
# Ensure instances have their own copy in case a plugin author
# mangles it.
if self.defaults is not None:
self.defaults = self.defaults.copy()
@property
def filename(self):
# type: () -> str
"""Return the view's file path or '' if unsaved."""
return self.view.file_name() or ''
@property
def executable_path(self):
deprecation_warning(
"{}: `executable_path` has been deprecated. "
"Just use an ordinary binary name instead. "
.format(self.name)
)
return getattr(self, 'executable', '')
def get_view_settings(self):
deprecation_warning(
"{}: `self.get_view_settings()` has been deprecated. "
"Just use the member `self.settings` which is the same thing."
.format(self.name)
)
return self.settings
def notify_failure(self):
# Side-effect: the status bar will show `(erred)`
window = self.view.window()
if window:
window.run_command('sublime_linter_failed', {
'filename': util.canonical_filename(self.view),
'linter_name': self.name
})
def notify_unassign(self):
# Side-effect: the status bar will not show the linter at all
window = self.view.window()
if window:
window.run_command('sublime_linter_unassigned', {
'filename': util.canonical_filename(self.view),
'linter_name': self.name
})
def on_stderr(self, output):
self.logger.warning('{} output:\n{}'.format(self.name, output))
self.logger.info(
'Note: above warning will become an error in the future. '
'Implement `on_stderr` if you think this is wrong.')
self.notify_failure()
def which(self, cmd):
"""Return full path to a given executable.
This version just delegates to `util.which` but plugin authors can
override this method.
"""
return util.which(cmd)
def get_cmd(self):
# type: () -> Optional[List[str]]
"""
Calculate and return a tuple/list of the command line to be executed.
The cmd class attribute may be a string, a tuple/list, or a callable.
If cmd is callable, it is called. If the result of the method is
a string, it is parsed into a list with shlex.split.
Otherwise the result of build_cmd is returned.
"""
assert self.cmd is not None
cmd = self.cmd
if callable(cmd):
cmd = cmd()
if isinstance(cmd, str):
cmd = shlex.split(cmd)
else:
cmd = list(cmd)
return self.build_cmd(cmd)
def build_cmd(self, cmd):
# type: (List[str]) -> Optional[List[str]]
"""
Return a tuple with the command line to execute.
Tries to find an executable with its complete path for cmd and replaces
cmd[0] with it.
The delegates to `insert_args` and returns whatever it returns.
"""
which = cmd[0]
have_path, path = self.context_sensitive_executable_path(cmd)
if have_path:
# happy path?
if path is None:
# Do not log, `context_sensitive_executable_path` should have
# logged already.
return None
else:
path = self.which(which)
if not path:
self.logger.warning(
"{} cannot locate '{}'\n"
"Please refer to the readme of this plugin and our troubleshooting guide: "
"http://www.sublimelinter.com/en/stable/troubleshooting.html"
.format(self.name, which)
)
return None
cmd[0:1] = util.ensure_list(path)
return self.insert_args(cmd)
def context_sensitive_executable_path(self, cmd):
# type: (List[str]) -> Tuple[bool, Union[None, str, List[str]]]
"""Calculate the context-sensitive executable path.
Subclasses may override this to return a special path. The default
implementation looks for a setting `executable` and if set will use
that.
Return (True, '<path>') if you can resolve the executable given at cmd[0]
Return (True, None) if you want to skip the linter
Return (False, None) if you want to kick in the default implementation
of SublimeLinter
Notable: `<path>` can be a list/tuple or str
"""
executable = self.settings.get('executable', None) # type: Union[None, str, List[str]]
if executable:
wanted_executable, *rest = util.ensure_list(executable)
resolved_executable = self.which(wanted_executable)
if not resolved_executable:
if os.path.isabs(wanted_executable):
message = (
"You set 'executable' to {!r}. "
"However, '{}' does not exist or is not executable. "
.format(executable, wanted_executable)
)
else:
message = (
"You set 'executable' to {!r}. "
"However, 'which {}' returned nothing.\n"
"Try setting an absolute path to the binary. "
"Also refer our troubleshooting guide: "
"http://www.sublimelinter.com/en/stable/troubleshooting.html"
.format(executable, wanted_executable)
)
self.logger.error(message)
self.notify_failure()
raise PermanentError()
self.logger.info(
"{}: wanted executable is {!r}".format(self.name, executable)
)
return True, [resolved_executable] + rest
return False, None
def insert_args(self, cmd):
# type: (List[str]) -> List[str]
"""Insert user arguments into cmd and return the result."""
args = self.build_args(self.settings)
if '${args}' in cmd:
i = cmd.index('${args}')
cmd[i:i + 1] = args
elif '*' in cmd:
deprecation_warning(
"{}: Usage of '*' as a special marker in `cmd` has been "
"deprecated, use '${{args}}' instead."
.format(self.name)
)
i = cmd.index('*')
cmd[i:i + 1] = args
else:
cmd += args
return cmd
def get_user_args(self, settings):
# type: (LinterSettings) -> List[str]
"""Return any args the user specifies in settings as a list."""
args = settings.get('args', [])
if isinstance(args, str):
args = shlex.split(args)
else:
args = args[:]
return args
def build_args(self, settings):
# type: (LinterSettings) -> List[str]
"""Return a list of args to add to cls.cmd.
This basically implements our DSL around arguments on the command
line. See `ARG_RE` and `LinterMeta.map_args`. All potential args
are defined in `cls.defaults` with a prefix of `-` or `--`.
(All other defaults are just normal settings.)
Note that all falsy values except the Zero are skipped. The value
`True` acts as a flag. In all other cases args are key value pairs.
"""
args = self.get_user_args(settings)
args_map = getattr(self, 'args_map', {})
for setting, arg_info in args_map.items():
prefix = arg_info['prefix']
if prefix is None:
continue
values = settings.get(setting, None)
if not values and type(values) is not int: # Allow `0`!
continue
arg = prefix + arg_info['name']