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

Disable selected transformers in comment disablers #667

Merged
merged 1 commit into from
Mar 12, 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 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(disablers.ALL_TRANSFORMERS):
return None
diff, _, new_model = robotidy_class.transform(model, disabler_finder.disablers)
if not diff:
Expand Down
11 changes: 7 additions & 4 deletions robotidy/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import sys
from difflib import unified_diff
from typing import Dict

try:
import rich_click as click
Expand All @@ -11,8 +10,8 @@
from robot.api import get_model
from robot.errors import DataError

from robotidy import disablers
from robotidy.config import MainConfig
from robotidy.disablers import RegisterDisablers
from robotidy.rich_console import console
from robotidy.utils import misc

Expand All @@ -35,7 +34,9 @@ def transform_files(self):
for source, config in self.main_config.get_sources_with_configs():
self.config = config
all_files += 1
disabler_finder = RegisterDisablers(self.config.formatting.start_line, self.config.formatting.end_line)
disabler_finder = disablers.RegisterDisablers(
self.config.formatting.start_line, self.config.formatting.end_line
)
previous_changed_files = changed_files
try:
stdin = False
Expand All @@ -49,7 +50,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(disablers.ALL_TRANSFORMERS):
continue
diff, old_model, new_model, model = self.transform_until_stable(model, disabler_finder)
if stdin:
Expand Down Expand Up @@ -109,6 +110,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
144 changes: 103 additions & 41 deletions robotidy/disablers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import functools
import re
from typing import Dict, List, Optional

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

ALL_TRANSFORMERS = "all"


def skip_if_disabled(func):
"""
Expand All @@ -12,7 +15,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 +43,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 +65,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_TRANSFORMERS: DisabledLines(start_line, end_line, file_end)}

def parse_global_disablers(self):
self.disablers[ALL_TRANSFORMERS].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].add_disabled_header(lineno)

def is_disabled_in_file(self, transformer_name: str) -> bool:
if self.disablers[ALL_TRANSFORMERS].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_TRANSFORMERS].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_TRANSFORMERS].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 +144,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 +163,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_TRANSFORMERS):
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_TRANSFORMERS]
# 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_TRANSFORMERS: 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_TRANSFORMERS: 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_TRANSFORMERS: 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 +240,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
Loading
Loading