Skip to content

Commit

Permalink
Disable selected transformers in comment disablers
Browse files Browse the repository at this point in the history
  • Loading branch information
bhirsz committed Mar 11, 2024
1 parent 77e9bfa commit 5d728a2
Show file tree
Hide file tree
Showing 25 changed files with 316 additions and 74 deletions.
9 changes: 9 additions & 0 deletions docs/releasenotes/unreleased/other.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Disable selected transformers (#653)
------------------------------------

Robotidy disablers now supports not only disabling all transformers but selected ones::

*** Test Cases ***
Test with mixed variables
Keyword call ${global} # robotidy: off = RenameVariables

14 changes: 14 additions & 0 deletions docs/source/configuration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,17 @@ You can also disable the formatting in the whole section if you put ``# robotidy
*** Keywords *** # robotidy: off
Not Formatted
Step
It is possible to disable only selected transformers by passing their names to disabler in comma separated list:

.. code-block:: robotframework
*** Test Cases ***
Formatted Partially
Step
... ${arg} # robotidy: off=AlignTestCasesSection,NormalizeSeparators
Step 2
*** Keywords *** # robotidy: off = NormalizeNewLines
Not Formatted
Step
2 changes: 1 addition & 1 deletion robotidy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def transform_model(model, root_dir: str, output: Optional[str] = None, **kwargs
robotidy_class.config.formatting.start_line, robotidy_class.config.formatting.end_line
)
disabler_finder.visit(model)
if disabler_finder.file_disabled:
if disabler_finder.is_disabled_in_file("all"):
return None
diff, _, new_model = robotidy_class.transform(model, disabler_finder.disablers)
if not diff:
Expand Down
4 changes: 3 additions & 1 deletion robotidy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def transform_files(self):
model = self.get_model(source)
model_path = model.source
disabler_finder.visit(model)
if disabler_finder.file_disabled:
if disabler_finder.is_disabled_in_file("all"):
continue
diff, old_model, new_model, model = self.transform_until_stable(model, disabler_finder)
if stdin:
Expand Down Expand Up @@ -109,6 +109,8 @@ def transform(self, model, disablers):
old_model = misc.StatementLinesCollector(model)
for transformer in self.config.transformers:
setattr(transformer, "disablers", disablers) # set dynamically to allow using external transformers
if disablers.is_disabled_in_file(transformer.__class__.__name__):
continue
transformer.visit(model)
new_model = misc.StatementLinesCollector(model)
return new_model != old_model, old_model, new_model
Expand Down
142 changes: 101 additions & 41 deletions robotidy/disablers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import re
from typing import Dict, List, Optional

from robot.api.parsing import Comment, CommentSection, ModelVisitor, Token

Expand All @@ -12,7 +13,8 @@ def skip_if_disabled(func):

@functools.wraps(func)
def wrapper(self, node, *args, **kwargs):
if self.disablers.is_node_disabled(node):
class_name = self.__class__.__name__
if self.disablers.is_node_disabled(class_name, node):
return node
return func(self, node, *args, **kwargs)

Expand All @@ -39,9 +41,10 @@ def skip_section_if_disabled(func):

@functools.wraps(func)
def wrapper(self, node, *args, **kwargs):
if self.disablers.is_node_disabled(node):
class_name = self.__class__.__name__
if self.disablers.is_node_disabled(class_name, node):
return node
if self.disablers.is_header_disabled(node.lineno):
if self.disablers.is_header_disabled(class_name, node.lineno):
return node
if self.skip:
section_name = get_section_name_from_header_type(node)
Expand All @@ -60,20 +63,62 @@ def is_line_start(node):
return False


class DisablersInFile:
def __init__(self, start_line: Optional[int], end_line: Optional[int], file_end: Optional[int] = None):
self.start_line = start_line
self.end_line = end_line
self.file_end = file_end
self.disablers = {"all": DisabledLines(start_line, end_line, file_end)}

def parse_global_disablers(self):
self.disablers["all"].parse_global_disablers()

def sort_disablers(self):
for disabled_lines in self.disablers.values():
disabled_lines.sort_disablers()

def add_disabler(self, transformer: str, start_line: int, end_line: int, file_level: bool = False):
if transformer not in self.disablers:
self.disablers[transformer] = DisabledLines(self.start_line, self.end_line, self.file_end)
self.disablers[transformer].add_disabler(start_line, end_line)
if file_level:
self.disablers[transformer].disabled_whole = file_level

def add_disabled_header(self, transformer: str, lineno):
if transformer not in self.disablers:
self.disablers[transformer] = DisabledLines(self.start_line, self.end_line, self.file_end)
self.disablers[transformer].disabled_headers.add(lineno)

def is_disabled_in_file(self, transformer_name: str) -> bool:
if self.disablers["all"].disabled_whole:
return True
if transformer_name not in self.disablers:
return False
return self.disablers[transformer_name].disabled_whole

def is_header_disabled(self, transformer_name: str, line) -> bool:
if self.disablers["all"].is_header_disabled(line):
return True
if transformer_name not in self.disablers:
return False
return self.disablers[transformer_name].is_header_disabled(line)

def is_node_disabled(self, transformer_name: str, node, full_match=True) -> bool:
if self.disablers["all"].is_node_disabled(node, full_match):
return True
if transformer_name not in self.disablers:
return False
return self.disablers[transformer_name].is_node_disabled(node, full_match)


class DisabledLines:
def __init__(self, start_line, end_line, file_end):
self.start_line = start_line
self.end_line = end_line
self.file_end = file_end
self.lines = []
self.disabled_headers = set()

@property
def file_disabled(self):
"""Check if file is disabled. Whole file is only disabled if the first line contains one line disabler."""
if not self.lines:
return False
return self.lines[0] == (1, 1)
self.disabled_whole = False

def add_disabler(self, start_line, end_line):
self.lines.append((start_line, end_line))
Expand All @@ -97,7 +142,7 @@ def is_header_disabled(self, line):
return line in self.disabled_headers

def is_node_disabled(self, node, full_match=True):
if not node:
if not node or not self.lines:
return False
end_lineno = max(node.lineno, node.end_lineno) # workaround for transformers setting -1 as end_lineno
if full_match:
Expand All @@ -116,61 +161,69 @@ class RegisterDisablers(ModelVisitor):
def __init__(self, start_line, end_line):
self.start_line = start_line
self.end_line = end_line
self.disablers = DisabledLines(self.start_line, self.end_line, None)
self.disabler_pattern = re.compile(r"\s*#\s?robotidy:\s?(?P<disabler>on|off)")
self.stack = []
self.file_disabled = False
self.disablers = DisablersInFile(start_line, end_line)
self.disabler_pattern = re.compile(r"\s*#\s?robotidy:\s?(?P<disabler>on|off) ?=?(?P<transformers>[\w,\s]*)")
self.disablers_in_scope: List[Dict[str, int]] = []
self.file_level_disablers = False

def any_disabler_open(self):
return any(disabler for disabler in self.stack)
def is_disabled_in_file(self, transformer_name: str = "all"):
return self.disablers.is_disabled_in_file(transformer_name)

def get_disabler(self, comment):
if not comment.value:
return None
return self.disabler_pattern.match(comment.value)

def close_disabler(self, end_line):
disabler = self.stack.pop()
if disabler:
if self.file_level_disablers:
self.file_disabled = True
self.disablers.add_disabler(disabler, end_line)
disabler = self.disablers_in_scope.pop()
for transformer_name, start_line in disabler.items():
if not start_line:
continue
self.disablers.add_disabler(transformer_name, start_line, end_line, self.file_level_disablers)

def visit_File(self, node): # noqa
self.file_level_disablers = False
self.disablers = DisabledLines(self.start_line, self.end_line, node.end_lineno)
self.disablers = DisablersInFile(self.start_line, self.end_line, node.end_lineno)
self.disablers.parse_global_disablers()
self.stack = []
for index, section in enumerate(node.sections):
self.file_level_disablers = index == 0 and isinstance(section, CommentSection)
self.visit_Section(section)
self.disablers.sort_disablers()
self.file_disabled = self.file_disabled or self.disablers.file_disabled

@staticmethod
def get_disabler_transformers(match) -> List[str]:
if not match.group("transformers") or "=" not in match.group(0): # robotidy: off or robotidy: off comment
return ["all"]
# robotidy: off=Transformer1, Transformer2
return [transformer.strip() for transformer in match.group("transformers").split(",") if transformer.strip()]

def visit_SectionHeader(self, node): # noqa
for comment in node.get_tokens(Token.COMMENT):
disabler = self.get_disabler(comment)
if disabler and disabler.group("disabler") == "off":
self.disablers.add_disabled_header(node.lineno)
break
if not disabler or disabler.group("disabler") != "off":
continue
transformers = self.get_disabler_transformers(disabler)
for transformer in transformers:
self.disablers.add_disabled_header(transformer, node.lineno)
break
return self.generic_visit(node)

def visit_TestCase(self, node): # noqa
self.stack.append(0)
self.disablers_in_scope.append({"all": 0})
self.generic_visit(node)
self.close_disabler(node.end_lineno)

def visit_Try(self, node): # noqa
self.generic_visit(node.header)
self.stack.append(0)
self.disablers_in_scope.append({"all": 0})
for statement in node.body:
self.visit(statement)
self.close_disabler(node.end_lineno)
tail = node
while tail.next:
self.generic_visit(tail.header)
self.stack.append(0)
self.disablers_in_scope.append({"all": 0})
for statement in tail.body:
self.visit(statement)
end_line = tail.next.lineno - 1 if tail.next else tail.end_lineno
Expand All @@ -185,19 +238,26 @@ def visit_Statement(self, node): # noqa
disabler = self.get_disabler(comment)
if not disabler:
return
transformers = self.get_disabler_transformers(disabler)
index = 0 if is_line_start(node) else -1
if disabler.group("disabler") == "on":
if not self.stack[index]: # no disabler open
return
self.disablers.add_disabler(self.stack[index], node.lineno)
self.stack[index] = 0
elif not self.stack[index]:
self.stack[index] = node.lineno
disabler_start = disabler.group("disabler") == "on"
for transformer in transformers:
if disabler_start:
start_line = self.disablers_in_scope[index].get(transformer)
if not start_line: # no disabler open
continue
self.disablers.add_disabler(transformer, start_line, node.lineno)
self.disablers_in_scope[index][transformer] = 0
else:
if not self.disablers_in_scope[index].get(transformer):
self.disablers_in_scope[index][transformer] = node.lineno
else:
# inline disabler
if self.any_disabler_open():
return
for comment in node.get_tokens(Token.COMMENT):
disabler = self.get_disabler(comment)
if disabler and disabler.group("disabler") == "off":
self.disablers.add_disabler(node.lineno, node.end_lineno)
if not disabler:
continue
transformers = self.get_disabler_transformers(disabler)
if disabler.group("disabler") == "off":
for transformer in transformers:
self.disablers.add_disabler(transformer, node.lineno, node.end_lineno)
2 changes: 1 addition & 1 deletion robotidy/transformers/AlignSettingsSection.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def __init__(
def visit_SettingSection(self, node): # noqa
statements = []
for child in node.body:
if self.disablers.is_node_disabled(child) or self.is_node_skip(child):
if self.disablers.is_node_disabled("AlignSettingsSection", child) or self.is_node_skip(child):
statements.append(child)
elif child.type in (Token.EOL, Token.COMMENT):
statements.append(misc.left_align(child))
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/AlignVariablesSection.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def should_parse(self, node):
def visit_VariableSection(self, node): # noqa
statements = []
for child in node.body:
if self.disablers.is_node_disabled(child):
if self.disablers.is_node_disabled("AlignVariablesSection", child):
statements.append(child)
elif child.type in (Token.EOL, Token.COMMENT):
statements.append(misc.left_align(child))
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/InlineIf.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def visit_Section(self, node): # noqa
def visit_If(self, node: If): # noqa
if node.errors or getattr(node.end, "errors", None):
return node
if self.disablers.is_node_disabled(node, full_match=False):
if self.disablers.is_node_disabled("InlineIf", node, full_match=False):
return node
if self.is_inline(node):
return self.handle_inline(node)
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/NormalizeAssignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def visit_VariableSection(self, node): # noqa
for child in node.body:
if not isinstance(child, Variable):
continue
if self.disablers.is_node_disabled(child):
if self.disablers.is_node_disabled("NormalizeAssignments", child):
continue
var_token = child.get_token(Token.VARIABLE)
self.normalize_equal_sign(
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/NormalizeTags.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def visit_DefaultTags(self, node): # noqa
visit_TestTags = visit_ForceTags = visit_DefaultTags

def normalize_tags(self, node, indent=False):
if self.disablers.is_node_disabled(node, full_match=False):
if self.disablers.is_node_disabled("NormalizeTags", node, full_match=False):
return node
if self.preserve_format:
return self.normalize_tags_tokens_preserve_formatting(node)
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/OrderTags.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def visit_ForceTags(self, node): # noqa
return self.order_tags(node) if self.force_tags else node

def order_tags(self, node, indent=False):
if self.disablers.is_node_disabled(node):
if self.disablers.is_node_disabled("OrderTags", node):
return node
ordered_tags = sorted(
(tag.value for tag in node.data_tokens[1:]),
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/RemoveEmptySettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def visit_Statement(self, node): # noqa
# when not setting type or setting type but not empty
if node.type not in Token.SETTING_TOKENS or len(node.data_tokens) != 1:
return node
if self.disablers.is_node_disabled(node):
if self.disablers.is_node_disabled("RemoveEmptySettings", node):
return node
# when empty and not overwriting anything - remove
if (
Expand Down
2 changes: 1 addition & 1 deletion robotidy/transformers/RenameVariables.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def visit_Keyword(self, node): # noqa

def visit_KeywordCall(self, node): # noqa
self.handle_set_local_variable(node)
if not self.disablers.is_node_disabled(node):
if not self.disablers.is_node_disabled("RenameVariables", node):
for token in node.data_tokens:
if token.type == Token.ASSIGN:
token.value = self.rename_value(token.value, variable_case="lower", is_var=False)
Expand Down
8 changes: 5 additions & 3 deletions robotidy/transformers/SplitTooLongLine.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,16 @@ def visit_KeywordCall(self, node): # noqa
return node
if not self.should_transform_node(node):
return node
if self.disablers.is_node_disabled(node, full_match=False):
if self.disablers.is_node_disabled("SplitTooLongLine", node, full_match=False):
return node
if self.is_run_keyword(node.keyword):
return node
return self.split_keyword_call(node)

def visit_Var(self, node): # noqa
if self.disablers.is_node_disabled(node, full_match=False) or not self.should_transform_node(node):
if self.disablers.is_node_disabled(
"SplitTooLongLine", node, full_match=False
) or not self.should_transform_node(node):
return node
var_name = node.get_token(Token.VARIABLE)
if not var_name:
Expand Down Expand Up @@ -209,7 +211,7 @@ def visit_ForceTags(self, node): # noqa
def split_setting_with_args(self, node, settings_section):
if not self.should_transform_node(node):
return node
if self.disablers.is_node_disabled(node, full_match=False):
if self.disablers.is_node_disabled("SplitTooLongLine", node, full_match=False):
return node
if settings_section:
indent = 0
Expand Down
Loading

0 comments on commit 5d728a2

Please sign in to comment.