├── .python-version ├── .gitignore ├── setup.cfg ├── messages.json ├── unittesting.json ├── messages └── install.txt ├── mypy.ini ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── linter.py └── tests └── test_regex.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt" 3 | } 4 | -------------------------------------------------------------------------------- /unittesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "deferred": true, 3 | "verbosity": 2, 4 | "capture_console": true 5 | } 6 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-annotations 2 | ------------------------------- 3 | This linter plugin for SublimeLinter highlights annotations in comments 4 | such as FIXME, NOTE, README, TODO, @todo, and XXX. 5 | 6 | For more information on configuring this linter: 7 | 8 | https://github.com/SublimeLinter/SublimeLinter-annotations 9 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | warn_redundant_casts = True 4 | warn_unused_ignores = True 5 | mypy_path = 6 | :$MYPY_CONFIG_FILE_DIR/../ 7 | :$MYPY_CONFIG_FILE_DIR/../SublimeLinter/stubs 8 | sqlite_cache = True 9 | 10 | [mypy-Default] 11 | ignore_missing_imports = True 12 | 13 | [mypy-unittesting] 14 | ignore_missing_imports = True 15 | 16 | [mypy-package_control] 17 | ignore_missing_imports = True 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | check-messages: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: kaste/upgrade-messages-test-action@v1 15 | 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: TrueBrain/actions-flake8@v2 21 | 22 | run-tests: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: SublimeText/UnitTesting/actions/setup@v1 27 | with: 28 | sublime-text-version: 4 29 | extra-packages: 30 | SublimeLinter/SublimeLinter 31 | - uses: SublimeText/UnitTesting/actions/run-tests@v1 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SublimeLinter-annotations 2 | ========================= 3 | 4 | [![Build Status](https://travis-ci.org/SublimeLinter/SublimeLinter-annotations.svg?branch=master)](https://travis-ci.org/SublimeLinter/SublimeLinter-annotations) 5 | 6 | This linter plugin for [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter) highlights annotations in comments such as FIXME, NOTE, TODO, @todo, XXX, and README. 7 | It will be used with all files. 8 | 9 | ## Installation 10 | SublimeLinter must be installed in order to use this plugin. 11 | 12 | Please use [Package Control](https://packagecontrol.io) to install the linter plugin. 13 | 14 | ## Settings 15 | - SublimeLinter settings: http://sublimelinter.com/en/latest/settings.html 16 | - Linter settings: http://sublimelinter.com/en/latest/linter_settings.html 17 | 18 | Additional SublimeLinter-annotations settings: 19 | 20 | |Setting|Description| 21 | |:------|:----------| 22 | |`warnings`|Comma-delimited list of words that will be highlighted as warnings.| 23 | |`errors`|Comma-delimited list of words that will be highlighted as errors.| 24 | |`infos`|Comma-delimited list of words that will be highlighted as infos.| 25 | |`mark_message`|Whether the rest of the comment line should be marked or just the word.| 26 | |`selector_` (*advanced*)| A scope selector for regions that the word lists will be searched in.| 27 | 28 | Matching is case-sensitive and matches whole words. 29 | 30 | For example: 31 | 32 | ```json 33 | "linters": { 34 | "annotations": { 35 | "infos": ["NOTA BENE", "FYI"], 36 | "warnings": ["FOO", "BAR"], 37 | "errors": ["WHAT?", "OMG!"] 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /linter.py: -------------------------------------------------------------------------------- 1 | # 2 | # linter.py 3 | # Linter for SublimeLinter3, a code checking framework for Sublime Text 3 4 | # 5 | # Written by Aparajita Fishman 6 | # Copyright (c) 2015-2016 The SublimeLinter Community 7 | # Copyright (c) 2013-2014 Aparajita Fishman 8 | # 9 | # License: MIT 10 | # 11 | 12 | """This module exports the Annotations plugin class.""" 13 | 14 | from itertools import accumulate, chain 15 | import re 16 | 17 | from SublimeLinter.lint import Linter, LintMatch 18 | 19 | 20 | MYPY = False 21 | if MYPY: 22 | from typing import Iterator, List, Union 23 | from SublimeLinter.lint import util 24 | 25 | 26 | def _escape_words(values): 27 | for value in values: 28 | # Add \b word separator fences around the value 29 | # if it begins or ends with a word character. 30 | value = re.escape(value) 31 | 32 | if value[0].isalnum() or value[0] == '_': 33 | value = r'\b' + value 34 | 35 | if value[-1].isalnum() or value[-1] == '_': 36 | value += r'\b' 37 | 38 | yield value 39 | 40 | 41 | class Annotations(Linter): 42 | """Discovers and marks FIXME, NOTE, README, TODO, @todo, and XXX annotations.""" 43 | 44 | cmd = None 45 | 46 | # We use this to do the matching 47 | mark_regex_template = ( 48 | r'(?P{}):?\s*'r'(?P.*)' 49 | ) 50 | 51 | # Words to look for 52 | defaults = { 53 | 'selector': '', # select all views 54 | 'errors': ['FIXME', 'ERROR'], 55 | 'warnings': [ 56 | 'TODO', '@todo', 'XXX', 'WIP', 'WARNING', 57 | 'todo!', # Rust macro 58 | ], 59 | 'infos': ['NOTE', 'README', 'INFO'], 60 | 'mark_message': False, 61 | 'selector_': 'comment - punctuation.definition.comment, support.macro.rust', 62 | } 63 | 64 | def run(self, cmd, code): 65 | # type: (Union[List[str], None], str) -> Union[util.popen_output, str] 66 | return 'something so SublimeLinter will not assume this view to be `ok`' 67 | 68 | def find_errors(self, output): 69 | # type: (str) -> Iterator[LintMatch] 70 | options = { 71 | option: '|'.join(_escape_words(self.settings[option])) 72 | for option in ('errors', 'warnings', 'infos') 73 | if self.settings[option] 74 | } 75 | if not options: 76 | return 77 | 78 | mark_regex = re.compile(self.mark_regex_template.format( 79 | "|".join( 80 | "(?P<{}>{})".format(key, value) 81 | for key, value in options.items() 82 | ) 83 | )) 84 | 85 | regions = self.view.find_by_selector(self.settings['selector_']) 86 | for region in regions: 87 | region_text = self.view.substr(region) 88 | lines = region_text.splitlines(keepends=True) 89 | offsets = accumulate(chain([region.a], map(len, lines))) 90 | for line, offset in zip(lines, offsets): 91 | match = mark_regex.search(line.rstrip()) 92 | if not match: 93 | continue 94 | 95 | message = match.group('message') or '' 96 | word = match.group('word') 97 | matched_groups = match.groupdict() 98 | error_type = singularize(next( 99 | group 100 | for group in ('errors', 'warnings', 'infos') 101 | if matched_groups.get(group) 102 | )) 103 | 104 | row, col = self.view.rowcol(offset + match.start()) 105 | text_to_mark = match.group() if self.settings.get('mark_message') else word 106 | yield LintMatch( 107 | line=row, 108 | col=col, 109 | near=text_to_mark, 110 | error_type=error_type, 111 | code=word, 112 | message=message 113 | ) 114 | 115 | 116 | def singularize(word): 117 | return { 118 | "errors": "error", 119 | "warnings": "warning", 120 | "infos": "info", 121 | }[word] 122 | -------------------------------------------------------------------------------- /tests/test_regex.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from unittesting import DeferrableTestCase 3 | from SublimeLinter.tests.parameterized import parameterized as p 4 | 5 | from SublimeLinter.lint import events, linter 6 | 7 | 8 | MYPY = False 9 | if MYPY: 10 | from typing import Generator 11 | 12 | 13 | class LintResultTestCase(DeferrableTestCase): 14 | def create_window(self): 15 | sublime.run_command("new_window") 16 | window = sublime.active_window() 17 | self.addCleanup(self.close_window, window) 18 | return window 19 | 20 | def close_window(self, window): 21 | window.run_command('close_window') 22 | 23 | def create_view(self, window): 24 | view = window.new_file() 25 | self.addCleanup(self.close_view, view) 26 | return view 27 | 28 | def close_view(self, view): 29 | view.set_scratch(True) 30 | view.close() 31 | 32 | def prepare_view(self, view_content, syntax): 33 | window = self.create_window() 34 | view = self.create_view(window) 35 | view.assign_syntax(syntax) 36 | view.run_command('append', {'characters': view_content}) 37 | return view 38 | 39 | def assertResult(self, result, expected): 40 | for actual, error in zip(result, expected): 41 | self.assertEqual({k: actual[k] for k in error.keys()}, error) 42 | 43 | def await_lint_result(self, view, linter_name_=None): 44 | # type: (sublime.View, str) -> Generator[object, object, list[dict]] 45 | if linter_name_ is None: 46 | linter_name_ = self.linter_name 47 | filename_ = linter.get_view_context(view)["canonical_filename"] 48 | actual = None 49 | 50 | @events.on("LINT_RESULT") 51 | def on_result(filename, linter_name, errors, **kwargs): 52 | # type: (str, str, list[dict], object) -> None 53 | nonlocal actual 54 | if linter_name == linter_name_ and filename == filename_: 55 | actual = errors 56 | 57 | self.addCleanup(events.off, on_result) 58 | 59 | yield lambda: actual is not None 60 | assert actual 61 | return actual 62 | 63 | 64 | class TestAnnotationsLinter(LintResultTestCase): 65 | linter_name = "annotations" 66 | 67 | @p.expand( 68 | [ 69 | ( 70 | "# {} The {} message".format(word, error_type), 71 | "scope:source.python", 72 | { 73 | "line": 0, 74 | "start": 2, 75 | "msg": "The {} message".format(error_type), 76 | "error_type": error_type, 77 | }, 78 | ) 79 | for error_type, words in ( 80 | ("error", ("FIXME", "ERROR")), 81 | ("warning", ("TODO", "@todo", "XXX", "WIP", "WARNING")), 82 | ("info", ("NOTE", "README", "INFO")), 83 | ) 84 | for word in words 85 | ] 86 | + [ 87 | ( # extract author of a note #33 88 | "// NOTE(kaste): a note", 89 | "scope:source.js", 90 | { 91 | "line": 0, 92 | "start": 3, 93 | "msg": "(kaste): a note", 94 | "error_type": "info", 95 | }, 96 | ) 97 | ] 98 | ) 99 | def test_end_to_end(self, view_content, syntax, expected): 100 | view = self.prepare_view(view_content, syntax) 101 | result = yield from self.await_lint_result(view) 102 | self.assertResult(result, [expected]) 103 | 104 | @p.expand( 105 | [ 106 | ( 107 | "# NOTE The note message\n" "# ERROR The error message\n", 108 | "scope:source.python", 109 | [ 110 | { 111 | "line": 1, 112 | "start": 2, 113 | "msg": "The error message", 114 | "error_type": "error", 115 | } 116 | ], 117 | ) 118 | ] 119 | ) 120 | def test_set_word_group_to_null_issue_39( 121 | self, view_content, syntax, expected 122 | ): 123 | view = self.prepare_view(view_content, syntax) 124 | view.settings().set("SublimeLinter.linters.annotations.infos", None) 125 | 126 | result = yield from self.await_lint_result(view) 127 | 128 | self.assertResult(result, expected) 129 | --------------------------------------------------------------------------------