|
3 | 3 | # This source code is licensed under the MIT license found in the
|
4 | 4 | # LICENSE file in the root directory of this source tree.
|
5 | 5 |
|
6 |
| -from typing import Generator, Optional, Sequence |
| 6 | +from typing import Generator, List, Optional, Sequence |
7 | 7 |
|
8 | 8 | from libcst import (
|
9 | 9 | BaseSuite,
|
|
12 | 12 | CSTNode,
|
13 | 13 | Decorator,
|
14 | 14 | EmptyLine,
|
| 15 | + ensure_type, |
15 | 16 | IndentedBlock,
|
16 | 17 | LeftSquareBracket,
|
| 18 | + matchers as m, |
17 | 19 | Module,
|
| 20 | + ParenthesizedWhitespace, |
18 | 21 | RightSquareBracket,
|
19 | 22 | SimpleStatementSuite,
|
| 23 | + SimpleWhitespace, |
20 | 24 | TrailingWhitespace,
|
21 | 25 | )
|
22 | 26 | from libcst.metadata import MetadataWrapper, ParentNodeProvider, PositionProvider
|
23 | 27 |
|
| 28 | +from .ftypes import LintIgnore, LintIgnoreStyle |
| 29 | + |
24 | 30 |
|
25 | 31 | def node_comments(
|
26 | 32 | node: CSTNode, metadata: MetadataWrapper
|
@@ -111,3 +117,104 @@ def gen(node: CSTNode) -> Generator[Comment, None, None]:
|
111 | 117 | # to only include comments that are located on or before the line containing
|
112 | 118 | # the original node that we're searching from
|
113 | 119 | yield from (c for c in gen(node) if positions[c].end.line <= target_line)
|
| 120 | + |
| 121 | + |
| 122 | +def node_nearest_comment(node: CSTNode, metadata: MetadataWrapper) -> CSTNode: |
| 123 | + """ |
| 124 | + Return the nearest tree node where a suppression comment could be added. |
| 125 | + """ |
| 126 | + parent_nodes = metadata.resolve(ParentNodeProvider) |
| 127 | + positions = metadata.resolve(PositionProvider) |
| 128 | + node_line = positions[node].start.line |
| 129 | + |
| 130 | + while not isinstance(node, Module): |
| 131 | + if hasattr(node, "comment"): |
| 132 | + return node |
| 133 | + |
| 134 | + if hasattr(node, "trailing_whitespace"): |
| 135 | + tw = ensure_type(node.trailing_whitespace, TrailingWhitespace) |
| 136 | + if tw and positions[tw].start.line == node_line: |
| 137 | + if tw.comment: |
| 138 | + return tw.comment |
| 139 | + else: |
| 140 | + return tw |
| 141 | + |
| 142 | + if hasattr(node, "comma"): |
| 143 | + if m.matches( |
| 144 | + node.comma, |
| 145 | + m.Comma( |
| 146 | + whitespace_after=m.ParenthesizedWhitespace( |
| 147 | + first_line=m.TrailingWhitespace() |
| 148 | + ) |
| 149 | + ), |
| 150 | + ): |
| 151 | + return ensure_type( |
| 152 | + node.comma.whitespace_after.first_line, TrailingWhitespace |
| 153 | + ) |
| 154 | + |
| 155 | + if hasattr(node, "rbracket"): |
| 156 | + tw = ensure_type( |
| 157 | + ensure_type( |
| 158 | + node.rbracket.whitespace_before, |
| 159 | + ParenthesizedWhitespace, |
| 160 | + ).first_line, |
| 161 | + TrailingWhitespace, |
| 162 | + ) |
| 163 | + if positions[tw].start.line == node_line: |
| 164 | + return tw |
| 165 | + |
| 166 | + if hasattr(node, "leading_lines"): |
| 167 | + return node |
| 168 | + |
| 169 | + parent = parent_nodes.get(node) |
| 170 | + if parent is None: |
| 171 | + break |
| 172 | + node = parent |
| 173 | + |
| 174 | + raise RuntimeError("could not find nearest comment node") |
| 175 | + |
| 176 | + |
| 177 | +def add_suppression_comment( |
| 178 | + module: Module, |
| 179 | + node: CSTNode, |
| 180 | + metadata: MetadataWrapper, |
| 181 | + name: str, |
| 182 | + style: LintIgnoreStyle = LintIgnoreStyle.fixme, |
| 183 | +) -> Module: |
| 184 | + """ |
| 185 | + Return a modified tree that includes a suppression comment for the given rule. |
| 186 | + """ |
| 187 | + # reuse an existing suppression directive if available rather than making a new one |
| 188 | + for comment in node_comments(node, metadata): |
| 189 | + lint_ignore = LintIgnore.parse(comment.value) |
| 190 | + if lint_ignore and lint_ignore.style == style: |
| 191 | + if name in lint_ignore.names: |
| 192 | + return module # already suppressed |
| 193 | + lint_ignore.names.add(name) |
| 194 | + return module.with_deep_changes(comment, value=str(lint_ignore)) |
| 195 | + |
| 196 | + # no existing directives, find the "nearest" location and add a comment there |
| 197 | + target = node_nearest_comment(node, metadata) |
| 198 | + lint_ignore = LintIgnore(style, {name}) |
| 199 | + |
| 200 | + if isinstance(target, Comment): |
| 201 | + lint_ignore.prefix = target.value.strip() |
| 202 | + return module.with_deep_changes(target, value=str(lint_ignore)) |
| 203 | + |
| 204 | + if isinstance(target, TrailingWhitespace): |
| 205 | + if target.comment: |
| 206 | + lint_ignore.prefix = target.comment.value.strip() |
| 207 | + return module.with_deep_changes(target.comment, value=str(lint_ignore)) |
| 208 | + else: |
| 209 | + return module.with_deep_changes( |
| 210 | + target, |
| 211 | + comment=Comment(str(lint_ignore)), |
| 212 | + whitespace=SimpleWhitespace(" "), |
| 213 | + ) |
| 214 | + |
| 215 | + if hasattr(target, "leading_lines"): |
| 216 | + ll: List[EmptyLine] = list(target.leading_lines or ()) |
| 217 | + ll.append(EmptyLine(comment=Comment(str(lint_ignore)))) |
| 218 | + return module.with_deep_changes(target, leading_lines=ll) |
| 219 | + |
| 220 | + raise RuntimeError("failed to add suppression comment") |
0 commit comments