Skip to content

Commit

Permalink
Support fixme's in docstrings (#9744)
Browse files Browse the repository at this point in the history
  • Loading branch information
badsketch authored Sep 30, 2024
1 parent c0ecd70 commit 1a96a5d
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 24 deletions.
9 changes: 9 additions & 0 deletions doc/user_guide/configuration/all-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,13 @@ Standard Checkers

``Miscellaneous`` **Checker**
-----------------------------
--check-fixme-in-docstring
""""""""""""""""""""""""""
*Whether or not to search for fixme's in docstrings.*

**Default:** ``False``


--notes
"""""""
*List of note tags to take in consideration, separated by a comma.*
Expand All @@ -1185,6 +1192,8 @@ Standard Checkers
.. code-block:: toml
[tool.pylint.miscellaneous]
check-fixme-in-docstring = false
notes = ["FIXME", "XXX", "TODO"]
notes-rgx = ""
Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/fragments/9255.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The `fixme` check can now search through docstrings as well as comments, by using
``check-fixme-in-docstring = true`` in the ``[tool.pylint.miscellaneous]`` section.

Closes #9255
72 changes: 57 additions & 15 deletions pylint/checkers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def process_module(self, node: nodes.Module) -> None:


class EncodingChecker(BaseTokenChecker, BaseRawFileChecker):
"""BaseChecker for encoding issues.
"""BaseChecker for encoding issues and fixme notes.
Checks for:
* warning notes in the code like FIXME, XXX
Expand Down Expand Up @@ -90,18 +90,37 @@ class EncodingChecker(BaseTokenChecker, BaseRawFileChecker):
"default": "",
},
),
(
"check-fixme-in-docstring",
{
"type": "yn",
"metavar": "<y or n>",
"default": False,
"help": "Whether or not to search for fixme's in docstrings.",
},
),
)

def open(self) -> None:
super().open()

notes = "|".join(re.escape(note) for note in self.linter.config.notes)
if self.linter.config.notes_rgx:
regex_string = rf"#\s*({notes}|{self.linter.config.notes_rgx})(?=(:|\s|\Z))"
else:
regex_string = rf"#\s*({notes})(?=(:|\s|\Z))"
notes += f"|{self.linter.config.notes_rgx}"

self._fixme_pattern = re.compile(regex_string, re.I)
comment_regex = rf"#\s*(?P<msg>({notes})(?=(:|\s|\Z)).*?$)"
self._comment_fixme_pattern = re.compile(comment_regex, re.I)

# single line docstring like '''this''' or """this"""
docstring_regex = rf"((\"\"\")|(\'\'\'))\s*(?P<msg>({notes})(?=(:|\s|\Z)).*?)((\"\"\")|(\'\'\'))"
self._docstring_fixme_pattern = re.compile(docstring_regex, re.I)

# multiline docstrings which will be split into newlines
# so we do not need to look for quotes/double-quotes
multiline_docstring_regex = rf"^\s*(?P<msg>({notes})(?=(:|\s|\Z)).*$)"
self._multiline_docstring_fixme_pattern = re.compile(
multiline_docstring_regex, re.I
)

def _check_encoding(
self, lineno: int, line: bytes, file_encoding: str
Expand Down Expand Up @@ -133,16 +152,39 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
if not self.linter.config.notes:
return
for token_info in tokens:
if token_info.type != tokenize.COMMENT:
continue
comment_text = token_info.string[1:].lstrip() # trim '#' and white-spaces
if self._fixme_pattern.search("#" + comment_text.lower()):
self.add_message(
"fixme",
col_offset=token_info.start[1] + 1,
args=comment_text,
line=token_info.start[0],
)
if token_info.type == tokenize.COMMENT:
if match := self._comment_fixme_pattern.match(token_info.string):
self.add_message(
"fixme",
col_offset=token_info.start[1] + 1,
args=match.group("msg"),
line=token_info.start[0],
)
elif self.linter.config.check_fixme_in_docstring:
if self._is_multiline_docstring(token_info):
docstring_lines = token_info.string.split("\n")
for line_no, line in enumerate(docstring_lines):
if match := self._multiline_docstring_fixme_pattern.match(line):
self.add_message(
"fixme",
col_offset=token_info.start[1] + 1,
args=match.group("msg"),
line=token_info.start[0] + line_no,
)
elif match := self._docstring_fixme_pattern.match(token_info.string):
self.add_message(
"fixme",
col_offset=token_info.start[1] + 1,
args=match.group("msg"),
line=token_info.start[0],
)

def _is_multiline_docstring(self, token_info: tokenize.TokenInfo) -> bool:
return (
token_info.type == tokenize.STRING
and (token_info.line.lstrip().startswith(('"""', "'''")))
and "\n" in token_info.line.rstrip()
)


def register(linter: PyLinter) -> None:
Expand Down
11 changes: 10 additions & 1 deletion tests/functional/f/fixme.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
"""Tests for fixme and its disabling and enabling."""
# pylint: disable=missing-function-docstring, unused-variable
# pylint: disable=missing-function-docstring, unused-variable, pointless-string-statement

# +1: [fixme]
# FIXME: beep
# +1: [fixme]
# TODO: don't forget indented ones should trigger
# +1: [fixme]
# TODO: that precedes another TODO: is treated as one and the message starts after the first
# +1: [fixme]
# TODO: large indentations after hash are okay

# but things cannot precede the TODO: do this

def function():
variable = "FIXME: Ignore me!"
Expand Down Expand Up @@ -35,6 +42,8 @@ def function():
# pylint: disable-next=fixme
# FIXME: Don't raise when the message is disabled

"""TODO: Don't raise when docstring fixmes are disabled"""

# This line needs to be at the end of the file to make sure it doesn't end with a comment
# Pragma's compare against the 'lineno' attribute of the respective nodes which
# would stop too soon otherwise.
Expand Down
19 changes: 11 additions & 8 deletions tests/functional/f/fixme.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
fixme:5:1:None:None::"FIXME: beep":UNDEFINED
fixme:11:20:None:None::"FIXME: Valid test":UNDEFINED
fixme:14:5:None:None::"TODO: Do something with the variables":UNDEFINED
fixme:16:18:None:None::"XXX: Fix this later":UNDEFINED
fixme:18:5:None:None::"FIXME: no space after hash":UNDEFINED
fixme:20:5:None:None::"todo: no space after hash":UNDEFINED
fixme:23:2:None:None::"FIXME: this is broken":UNDEFINED
fixme:25:5:None:None::"./TODO: find with notes":UNDEFINED
fixme:27:5:None:None::"TO make something DO: find with regex":UNDEFINED
fixme:7:5:None:None::"TODO: don't forget indented ones should trigger":UNDEFINED
fixme:9:1:None:None::"TODO: that precedes another TODO: is treated as one and the message starts after the first":UNDEFINED
fixme:11:1:None:None::"TODO: large indentations after hash are okay":UNDEFINED
fixme:18:20:None:None::"FIXME: Valid test":UNDEFINED
fixme:21:5:None:None::"TODO: Do something with the variables":UNDEFINED
fixme:23:18:None:None::"XXX: Fix this later":UNDEFINED
fixme:25:5:None:None::"FIXME: no space after hash":UNDEFINED
fixme:27:5:None:None::"todo: no space after hash":UNDEFINED
fixme:30:2:None:None::"FIXME: this is broken":UNDEFINED
fixme:32:5:None:None::"./TODO: find with notes":UNDEFINED
fixme:34:5:None:None::"TO make something DO: find with regex":UNDEFINED
56 changes: 56 additions & 0 deletions tests/functional/f/fixme_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Tests for fixme in docstrings"""
# pylint: disable=missing-function-docstring, pointless-string-statement

# +1: [fixme]
"""TODO resolve this"""
# +1: [fixme]
""" TODO: indentations are permitted """
# +1: [fixme]
''' TODO: indentations are permitted '''
# +1: [fixme]
""" TODO: indentations are permitted"""

""" preceding text TODO: is not permitted"""

"""
FIXME don't forget this # [fixme]
XXX also remember this # [fixme]
FIXME: and this line, but treat it as one FIXME TODO # [fixme]
text cannot precede the TODO: it must be at the start
XXX indentations are okay # [fixme]
??? the codetag must be recognized
"""

# +1: [fixme]
# FIXME should still work

# +1: [fixme]
# TODO """ should work

# """ TODO will not work
"""# TODO will not work"""

"""TODOist API should not result in a message"""

# +2: [fixme]
"""
TO make something DO: look a regex
"""

# pylint: disable-next=fixme
"""TODO won't work anymore"""

# +2: [fixme]
def function():
"""./TODO implement this"""


'''
XXX single quotes should be no different # [fixme]
'''
def function2():
'''
./TODO implement this # [fixme]
FIXME and this # [fixme]
'''
'''FIXME one more for good measure''' # [fixme]
7 changes: 7 additions & 0 deletions tests/functional/f/fixme_docstring.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=XXX,TODO,./TODO
# Regular expression of note tags to take in consideration.
notes-rgx=FIXME(?!.*ISSUE-\d+)|TO.*DO
# enable checking for fixme's in docstrings
check-fixme-in-docstring=yes
16 changes: 16 additions & 0 deletions tests/functional/f/fixme_docstring.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
fixme:5:1:None:None::TODO resolve this:UNDEFINED
fixme:7:1:None:None::"TODO: indentations are permitted ":UNDEFINED
fixme:9:1:None:None::"TODO: indentations are permitted ":UNDEFINED
fixme:11:1:None:None::"TODO: indentations are permitted":UNDEFINED
fixme:16:1:None:None::FIXME don't forget this # [fixme]:UNDEFINED
fixme:17:1:None:None::XXX also remember this # [fixme]:UNDEFINED
fixme:18:1:None:None::"FIXME: and this line, but treat it as one FIXME TODO # [fixme]":UNDEFINED
fixme:20:1:None:None::XXX indentations are okay # [fixme]:UNDEFINED
fixme:25:1:None:None::FIXME should still work:UNDEFINED
fixme:28:1:None:None::"TODO """""" should work":UNDEFINED
fixme:37:1:None:None::"TO make something DO: look a regex":UNDEFINED
fixme:45:5:None:None::./TODO implement this:UNDEFINED
fixme:49:1:None:None::XXX single quotes should be no different # [fixme]:UNDEFINED
fixme:53:5:None:None::./TODO implement this # [fixme]:UNDEFINED
fixme:54:5:None:None::FIXME and this # [fixme]:UNDEFINED
fixme:56:5:None:None::FIXME one more for good measure:UNDEFINED

0 comments on commit 1a96a5d

Please sign in to comment.