Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support fixme's in docstrings #9744

Merged
merged 15 commits into from
Sep 30, 2024
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
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
Loading