Skip to content

Commit 1a96a5d

Browse files
authored
Support fixme's in docstrings (#9744)
1 parent c0ecd70 commit 1a96a5d

File tree

8 files changed

+170
-24
lines changed

8 files changed

+170
-24
lines changed

doc/user_guide/configuration/all-options.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,13 @@ Standard Checkers
11601160

11611161
``Miscellaneous`` **Checker**
11621162
-----------------------------
1163+
--check-fixme-in-docstring
1164+
""""""""""""""""""""""""""
1165+
*Whether or not to search for fixme's in docstrings.*
1166+
1167+
**Default:** ``False``
1168+
1169+
11631170
--notes
11641171
"""""""
11651172
*List of note tags to take in consideration, separated by a comma.*
@@ -1185,6 +1192,8 @@ Standard Checkers
11851192
.. code-block:: toml
11861193
11871194
[tool.pylint.miscellaneous]
1195+
check-fixme-in-docstring = false
1196+
11881197
notes = ["FIXME", "XXX", "TODO"]
11891198
11901199
notes-rgx = ""

doc/whatsnew/fragments/9255.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The `fixme` check can now search through docstrings as well as comments, by using
2+
``check-fixme-in-docstring = true`` in the ``[tool.pylint.miscellaneous]`` section.
3+
4+
Closes #9255

pylint/checkers/misc.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def process_module(self, node: nodes.Module) -> None:
5151

5252

5353
class EncodingChecker(BaseTokenChecker, BaseRawFileChecker):
54-
"""BaseChecker for encoding issues.
54+
"""BaseChecker for encoding issues and fixme notes.
5555
5656
Checks for:
5757
* warning notes in the code like FIXME, XXX
@@ -90,18 +90,37 @@ class EncodingChecker(BaseTokenChecker, BaseRawFileChecker):
9090
"default": "",
9191
},
9292
),
93+
(
94+
"check-fixme-in-docstring",
95+
{
96+
"type": "yn",
97+
"metavar": "<y or n>",
98+
"default": False,
99+
"help": "Whether or not to search for fixme's in docstrings.",
100+
},
101+
),
93102
)
94103

95104
def open(self) -> None:
96105
super().open()
97106

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

104-
self._fixme_pattern = re.compile(regex_string, re.I)
111+
comment_regex = rf"#\s*(?P<msg>({notes})(?=(:|\s|\Z)).*?$)"
112+
self._comment_fixme_pattern = re.compile(comment_regex, re.I)
113+
114+
# single line docstring like '''this''' or """this"""
115+
docstring_regex = rf"((\"\"\")|(\'\'\'))\s*(?P<msg>({notes})(?=(:|\s|\Z)).*?)((\"\"\")|(\'\'\'))"
116+
self._docstring_fixme_pattern = re.compile(docstring_regex, re.I)
117+
118+
# multiline docstrings which will be split into newlines
119+
# so we do not need to look for quotes/double-quotes
120+
multiline_docstring_regex = rf"^\s*(?P<msg>({notes})(?=(:|\s|\Z)).*$)"
121+
self._multiline_docstring_fixme_pattern = re.compile(
122+
multiline_docstring_regex, re.I
123+
)
105124

106125
def _check_encoding(
107126
self, lineno: int, line: bytes, file_encoding: str
@@ -133,16 +152,39 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
133152
if not self.linter.config.notes:
134153
return
135154
for token_info in tokens:
136-
if token_info.type != tokenize.COMMENT:
137-
continue
138-
comment_text = token_info.string[1:].lstrip() # trim '#' and white-spaces
139-
if self._fixme_pattern.search("#" + comment_text.lower()):
140-
self.add_message(
141-
"fixme",
142-
col_offset=token_info.start[1] + 1,
143-
args=comment_text,
144-
line=token_info.start[0],
145-
)
155+
if token_info.type == tokenize.COMMENT:
156+
if match := self._comment_fixme_pattern.match(token_info.string):
157+
self.add_message(
158+
"fixme",
159+
col_offset=token_info.start[1] + 1,
160+
args=match.group("msg"),
161+
line=token_info.start[0],
162+
)
163+
elif self.linter.config.check_fixme_in_docstring:
164+
if self._is_multiline_docstring(token_info):
165+
docstring_lines = token_info.string.split("\n")
166+
for line_no, line in enumerate(docstring_lines):
167+
if match := self._multiline_docstring_fixme_pattern.match(line):
168+
self.add_message(
169+
"fixme",
170+
col_offset=token_info.start[1] + 1,
171+
args=match.group("msg"),
172+
line=token_info.start[0] + line_no,
173+
)
174+
elif match := self._docstring_fixme_pattern.match(token_info.string):
175+
self.add_message(
176+
"fixme",
177+
col_offset=token_info.start[1] + 1,
178+
args=match.group("msg"),
179+
line=token_info.start[0],
180+
)
181+
182+
def _is_multiline_docstring(self, token_info: tokenize.TokenInfo) -> bool:
183+
return (
184+
token_info.type == tokenize.STRING
185+
and (token_info.line.lstrip().startswith(('"""', "'''")))
186+
and "\n" in token_info.line.rstrip()
187+
)
146188

147189

148190
def register(linter: PyLinter) -> None:

tests/functional/f/fixme.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
"""Tests for fixme and its disabling and enabling."""
2-
# pylint: disable=missing-function-docstring, unused-variable
2+
# pylint: disable=missing-function-docstring, unused-variable, pointless-string-statement
33

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

13+
# but things cannot precede the TODO: do this
714

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

45+
"""TODO: Don't raise when docstring fixmes are disabled"""
46+
3847
# This line needs to be at the end of the file to make sure it doesn't end with a comment
3948
# Pragma's compare against the 'lineno' attribute of the respective nodes which
4049
# would stop too soon otherwise.

tests/functional/f/fixme.txt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
fixme:5:1:None:None::"FIXME: beep":UNDEFINED
2-
fixme:11:20:None:None::"FIXME: Valid test":UNDEFINED
3-
fixme:14:5:None:None::"TODO: Do something with the variables":UNDEFINED
4-
fixme:16:18:None:None::"XXX: Fix this later":UNDEFINED
5-
fixme:18:5:None:None::"FIXME: no space after hash":UNDEFINED
6-
fixme:20:5:None:None::"todo: no space after hash":UNDEFINED
7-
fixme:23:2:None:None::"FIXME: this is broken":UNDEFINED
8-
fixme:25:5:None:None::"./TODO: find with notes":UNDEFINED
9-
fixme:27:5:None:None::"TO make something DO: find with regex":UNDEFINED
2+
fixme:7:5:None:None::"TODO: don't forget indented ones should trigger":UNDEFINED
3+
fixme:9:1:None:None::"TODO: that precedes another TODO: is treated as one and the message starts after the first":UNDEFINED
4+
fixme:11:1:None:None::"TODO: large indentations after hash are okay":UNDEFINED
5+
fixme:18:20:None:None::"FIXME: Valid test":UNDEFINED
6+
fixme:21:5:None:None::"TODO: Do something with the variables":UNDEFINED
7+
fixme:23:18:None:None::"XXX: Fix this later":UNDEFINED
8+
fixme:25:5:None:None::"FIXME: no space after hash":UNDEFINED
9+
fixme:27:5:None:None::"todo: no space after hash":UNDEFINED
10+
fixme:30:2:None:None::"FIXME: this is broken":UNDEFINED
11+
fixme:32:5:None:None::"./TODO: find with notes":UNDEFINED
12+
fixme:34:5:None:None::"TO make something DO: find with regex":UNDEFINED

tests/functional/f/fixme_docstring.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for fixme in docstrings"""
2+
# pylint: disable=missing-function-docstring, pointless-string-statement
3+
4+
# +1: [fixme]
5+
"""TODO resolve this"""
6+
# +1: [fixme]
7+
""" TODO: indentations are permitted """
8+
# +1: [fixme]
9+
''' TODO: indentations are permitted '''
10+
# +1: [fixme]
11+
""" TODO: indentations are permitted"""
12+
13+
""" preceding text TODO: is not permitted"""
14+
15+
"""
16+
FIXME don't forget this # [fixme]
17+
XXX also remember this # [fixme]
18+
FIXME: and this line, but treat it as one FIXME TODO # [fixme]
19+
text cannot precede the TODO: it must be at the start
20+
XXX indentations are okay # [fixme]
21+
??? the codetag must be recognized
22+
"""
23+
24+
# +1: [fixme]
25+
# FIXME should still work
26+
27+
# +1: [fixme]
28+
# TODO """ should work
29+
30+
# """ TODO will not work
31+
"""# TODO will not work"""
32+
33+
"""TODOist API should not result in a message"""
34+
35+
# +2: [fixme]
36+
"""
37+
TO make something DO: look a regex
38+
"""
39+
40+
# pylint: disable-next=fixme
41+
"""TODO won't work anymore"""
42+
43+
# +2: [fixme]
44+
def function():
45+
"""./TODO implement this"""
46+
47+
48+
'''
49+
XXX single quotes should be no different # [fixme]
50+
'''
51+
def function2():
52+
'''
53+
./TODO implement this # [fixme]
54+
FIXME and this # [fixme]
55+
'''
56+
'''FIXME one more for good measure''' # [fixme]

tests/functional/f/fixme_docstring.rc

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

0 commit comments

Comments
 (0)