Skip to content
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
17 changes: 15 additions & 2 deletions docs/configuration/disablers.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,16 @@ Ignore whole blocks of code by defining a disabler in the new line:

```text
# robocop: off=rule1
Statement 1
Statement 2
```

Enable it again with ``on`` command:

```text
# robocop: off=rule1
Statement 1
Statement 2
# robocop: on=rule1
```

Expand Down Expand Up @@ -88,6 +93,15 @@ Compare Table With CSV Files
Should Be Equal ${erorrs} @{EMPTY}
```

``# robocop: off=wrong-case-in-keyword-name`` disabler can be also used in the FOR loop header and it will be applied
for the whole loop:

```robotframework
FOR ${file} IN @{files} # robocop: off=wrong-case-in-keyword-name
Table data should be in file ${data} ${file}
END
```

## Disabling files

It is possible to ignore a whole file if you put Robocop disabler in the first comment section, at the beginning of the
Expand All @@ -103,8 +117,7 @@ Some Test
Keyword 3

*** Keywords ***
Keyword 1
# robocop: off
Keyword 1 # robocop: off
Log 1

Keyword 2
Expand Down
20 changes: 20 additions & 0 deletions docs/releasenotes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@

### Features

- Extend robocop disablers to the whole node ([issue #1515](https://github.com/MarketSquare/robotframework-robocop/issues/1515)

Robocop will now ignore issues in the whole node (keyword, test case, for loop, keyword call, etc.) when the disabler
is set in the header / keyword call body. For example:

```robotframework
*** Keywords ***
My Keyword
FOR ${var} IN 1 2 3 # robocop: off=unused-variable
Log 1
END
Keyword # robocop: off=bad-indent
... ${var}
... ${var2}
```

Previously, Robocop would ignore ``unused-variable`` only when reported on the ``FOR`` header and ``bad-indent`` only
when reported on the first line of the ``Keyword`` call. After this change, those issues will be ignored in the whole
FOR loop and the whole ``Keyword`` call respectively.

- Ignore unused variables starting with ``_`` (``${_variable}``) ([issue #1457](https://github.com/MarketSquare/robotframework-robocop/issues/1457)

### Fixes
Expand Down
78 changes: 47 additions & 31 deletions src/robocop/linter/utils/disablers.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ def end_block(self, end_line: int) -> None:
self.blocks.append(self.current_block)
self.current_block = None

def add_inline_disabler(self, start_line: int, directive_col_start: int, directive_col_end: int) -> None:
self.lines[start_line] = Disabler(
def disable_block(self, start_line: int, end_line: int, directive_col_start: int, directive_col_end: int) -> None:
disabler = Disabler(
start_line=start_line,
end_line=start_line,
end_line=end_line,
directive_col_start=directive_col_start,
directive_col_end=directive_col_end,
)
for line in range(start_line, end_line + 1):
self.lines[line] = disabler

@property
def not_used_disablers(self) -> Generator[Disabler, None, None]:
Expand Down Expand Up @@ -96,7 +98,8 @@ def __init__(self, model: File):
self.file_end = 1
self.is_first_comment_section = True
self.keyword_or_test_section = False
self.last_name_header_line = 0
self.header_line_numer = 0
self.header_last_line = 0
self.disablers_in_scope = []
self.disabler_pattern = re.compile(
r"robocop: ?(?P<disabler>off|on)(?:\s?=\s?(?P<rules>[\w\-]+(?:,\s?[\w\-]+)*))?"
Expand All @@ -109,6 +112,8 @@ def visit_File(self, node: File) -> None: # noqa: N802
self.generic_visit(node)

def parse_disablers_in_node(self, node: type[Node], last_line: int | None = None) -> None:
self.header_line_numer = node.lineno
self.header_last_line = node.end_lineno
self.disablers_in_scope.append(defaultdict(lambda: RuleDisablers()))
self.generic_visit(node)
for rule_name, rule_disabler in self.disablers_in_scope[-1].items():
Expand All @@ -125,6 +130,7 @@ def parse_disablers_in_node(self, node: type[Node], last_line: int | None = None
self.disablers_in_scope.pop()

def visit_KeywordSection(self, node: KeywordSection | TestCaseSection) -> None: # noqa: N802
self.is_first_comment_section = False
self.keyword_or_test_section = True
self.parse_disablers_in_node(node)
self.keyword_or_test_section = False
Expand All @@ -136,61 +142,70 @@ def visit_Section(self, node: type[Node]) -> None: # noqa: N802
self.parse_disablers_in_node(node)
self.is_first_comment_section = False

visit_TestCase = visit_Keyword = visit_Try = visit_For = visit_ForLoop = visit_While = visit_Group = visit_Section # noqa: N815
def visit_TestCase(self, node: type[Node]) -> None: # noqa: N802
self.parse_disablers_in_node(node)

visit_Keyword = visit_Try = visit_For = visit_ForLoop = visit_While = visit_Group = visit_TestCase # noqa: N815

def visit_If(self, node: type[Node]) -> None: # noqa: N802
last_line = node.body[-1].end_lineno if node.body else None
self.parse_disablers_in_node(node, last_line)

def visit_Statement(self, node: Statement) -> None: # noqa: N802
for comment in node.get_tokens(Token.COMMENT):
self.parse_comment_token(comment, is_inline=True)
self.parse_comment_token(comment, parent_node=node, is_inline=False)

def visit_TestCaseName(self, node: KeywordName | TestCaseName) -> None: # noqa: N802
"""Save last test case / keyword header line number to check if comment is standalone."""
self.last_name_header_line = node.lineno
"""Save the last test case / keyword header line number to check if the comment is standalone."""
self.header_line_numer = node.lineno
self.visit_Statement(node)

visit_KeywordName = visit_TestCaseName # noqa: N815

def visit_Comment(self, node: Comment) -> None: # noqa: N802
for comment in node.get_tokens(Token.COMMENT):
# Comment is only inline if it is next to test/kw name
is_inline = comment.lineno == self.last_name_header_line
self.parse_comment_token(comment, is_inline=is_inline)
self.parse_comment_token(comment, is_inline=True)

def parse_comment_token(self, token: Token, is_inline: bool) -> None:
def parse_comment_token(self, token: Token, is_inline: bool, parent_node=None) -> None:
if "#" not in token.value:
return
disablers = []

if "# noqa" in token.value:
col_start = token.col_offset + token.value.index("# noqa") + 1
col_end = col_start + 6
self.rules["all"].add_inline_disabler(
start_line=token.lineno, directive_col_start=col_start, directive_col_end=col_end
)
disablers.append(("all", col_start, col_end))

for disabler in self.disabler_pattern.finditer(token.value):
col_start = token.col_offset + disabler.lastindex + 1
col_end = token.col_offset + disabler.endpos + 1
if col_end > token.end_col_offset + 1:
raise AssertionError
if not disabler.group("rules"):
col_end = min(col_end, token.end_col_offset + 1)

if not disabler.group("rules") or "noqa" in disabler.group("rules"):
rules = ["all"]
else:
rules = [rule.strip() for rule in disabler.group("rules").split(",") if rule.strip()]
if disabler.group("disabler") == "off":
for rule in rules:
if is_inline:
self.rules[rule].add_inline_disabler(
start_line=token.lineno, directive_col_start=col_start, directive_col_end=col_end
)
else:
scope = self.get_scope_for_disabler(token)
scope[rule].start_block(
start_line=token.lineno, directive_col_start=col_start, directive_col_end=col_end
)
elif disabler.group("disabler") == "on" and not is_inline:
disablers.extend([(rule, col_start, col_end) for rule in rules])
elif disabler.group("disabler") == "on" and is_inline:
scope = self.get_scope_for_disabler(token)
self.end_blocks(scope, rules, end_line=token.lineno)
for rule, col_start, col_end in disablers:
if is_inline or self.is_first_comment_section:
scope = self.get_scope_for_disabler(token)
scope[rule].start_block(
start_line=token.lineno, directive_col_start=col_start, directive_col_end=col_end
)
else:
if token.lineno == self.header_line_numer:
start_line, end_line = self.header_line_numer, self.header_last_line
elif parent_node:
start_line, end_line = parent_node.lineno, parent_node.end_lineno
else:
start_line, end_line = token.lineno, token.lineno
self.rules[rule].disable_block(
start_line=start_line, end_line=end_line, directive_col_start=col_start, directive_col_end=col_end
)

def get_scope_for_disabler(self, token: Token) -> defaultdict[str, RuleDisablers]:
if token.col_offset == 0 and self.keyword_or_test_section:
Expand Down Expand Up @@ -221,7 +236,7 @@ def file_disabled(self) -> bool:

def is_rule_disabled(self, diagnostic: Diagnostic) -> bool:
"""
Check if given `rule_msg` is disabled.
Check if the given ` rule_msg ` is disabled.

'All' takes precedence, then line disablers, then block disablers.
We're checking for both message id and name.
Expand All @@ -236,7 +251,7 @@ def is_rule_disabled(self, diagnostic: Diagnostic) -> bool:
)

def is_line_disabled(self, line: int, rule: str) -> bool:
"""Check if given line is in range of any disabled block"""
"""Check if a given line is in range of any disabled block"""
if rule not in self.visitor.rules:
return False
return line in self.visitor.rules[rule]
Expand All @@ -246,3 +261,4 @@ def not_used_disablers(self) -> Generator[tuple[str, Disabler], None, None]:
for rule, rule_disabler in self.visitor.rules.items():
for disabler in rule_disabler.not_used_disablers:
yield rule, disabler
disabler.used = True # for disablers spanning multiple lines
9 changes: 8 additions & 1 deletion tests/linter/rules/lengths/line_too_long/test.robot
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,11 @@ Keyword

Keyword With Unicode
This Is ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź ąęłżź
日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語
日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語 日本語

Multiline with disabler
[Documentation] Issue
VAR ${test} # robocop: off=line-too-long
... verylonglinelonglinglonglinelineverylonglinelonglinglonglinelineverylonglinelonglinglonglinelineverylonglinelonglinglonglineline
Append To List ${test} # robocop: off=line-too-long
... verylonglinelonglinglonglinelineverylonglinelonglinglonglinelineverylonglinelonglinglonglinelineverylonglinelonglinglonglineline
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ bug375.robot:30:1 [W] SPC08 Line is over-indented
bug375.robot:31:1 [W] SPC08 Line is over-indented
bug375.robot:32:1 [W] SPC08 Line is over-indented
bug375.robot:34:1 [W] SPC08 Line is over-indented
bug375.robot:39:1 [W] SPC08 Line is over-indented
bug375.robot:40:1 [W] SPC08 Line is over-indented
bug375.robot:44:1 [W] SPC08 Line is over-indented
bug375.robot:45:1 [W] SPC08 Line is over-indented
comments.robot:19:1 [W] SPC08 Line is over-indented
comments.robot:21:1 [W] SPC08 Line is over-indented
comments.robot:22:1 [W] SPC08 Line is over-indented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ bug375.robot:30:1 [W] SPC08 Line is over-indented
bug375.robot:31:1 [W] SPC08 Line is over-indented
bug375.robot:32:1 [W] SPC08 Line is over-indented
bug375.robot:34:1 [W] SPC08 Line is over-indented
bug375.robot:39:1 [W] SPC08 Line is over-indented
bug375.robot:40:1 [W] SPC08 Line is over-indented
bug375.robot:44:1 [W] SPC08 Line is over-indented
bug375.robot:45:1 [W] SPC08 Line is over-indented
comments.robot:19:1 [W] SPC08 Line is over-indented
comments.robot:21:1 [W] SPC08 Line is over-indented
comments.robot:22:1 [W] SPC08 Line is over-indented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ bug375.robot:30:1 [W] SPC08 Line is over-indented
bug375.robot:31:1 [W] SPC08 Line is over-indented
bug375.robot:32:1 [W] SPC08 Line is over-indented
bug375.robot:34:1 [W] SPC08 Line is over-indented
bug375.robot:39:1 [W] SPC08 Line is over-indented
bug375.robot:40:1 [W] SPC08 Line is over-indented
bug375.robot:44:1 [W] SPC08 Line is over-indented
bug375.robot:45:1 [W] SPC08 Line is over-indented
comments.robot:19:1 [W] SPC08 Line is over-indented
comments.robot:21:1 [W] SPC08 Line is over-indented
comments.robot:22:1 [W] SPC08 Line is over-indented
Expand Down
9 changes: 9 additions & 0 deletions tests/linter/test_data/disablers/disabled.robot
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ Test 1 # robocop: off=LEN08
Keyword1 # robocop: off=line-too-long
Log 1 # robocop: off=some-rule
No Operation # noqa

Multiline
Keyword
... ${ARG} # robocop: off=disable-whole-keyword
No Operation

*** Keywords *** # robocop: off=whole-section
Keyword
No Operation
8 changes: 7 additions & 1 deletion tests/linter/test_disablers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ def test_is_line_disabled(self):
assert disabler.any_disabler
assert disabler.is_line_disabled(10, "line-too-long")
assert disabler.is_line_disabled(12, "all") # from noqa
assert disabler.is_line_disabled(15, "disable-whole-keyword")
assert disabler.is_line_disabled(16, "disable-whole-keyword")
assert not disabler.is_line_disabled(17, "disable-whole-keyword")
assert disabler.is_line_disabled(19, "whole-section")
assert disabler.is_line_disabled(20, "whole-section")
assert disabler.is_line_disabled(21, "whole-section")
assert not disabler.is_line_disabled(10, "otherule")
model = get_model(DISABLED_TEST_DIR / "disabled_whole.robot")
disabler = DisablersFinder(model)
Expand All @@ -43,7 +49,7 @@ def test_is_line_disabled(self):

def test_is_rule_disabled(self, diagnostic):
# check if rule 1010 is disabled in selected lines
exp_disabled_lines = {6, 10, 12}
exp_disabled_lines = {6, 7, 8, 10, 11, 12, 13}
model = get_model(DISABLED_TEST_DIR / "disabled.robot")
disabler = DisablersFinder(model)
disabled_lines = set()
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.