Repository: MarketSquare/robotframework-robocop
Pull request #1346 (2025-03-24 13:56:25)

Reimplement list formatters in Robocop


Patch:
diff --git a/docs/source/formatters/formatters_list/AlignKeywordsSection.rst b/docs/source/formatters/formatters_list/AlignKeywordsSection.rst
index 0524fc3..b7c5113 100644
--- a/docs/source/formatters/formatters_list/AlignKeywordsSection.rst
+++ b/docs/source/formatters/formatters_list/AlignKeywordsSection.rst
@@ -174,7 +174,6 @@ misaligned in a row, the ``overflow`` mode is used instead of ``compact_overflow
 This behaviour helps in avoiding the situation, where ``compact_overflow`` misaligned whole line if most of the tokens
 does not fit in the column.
 
-# TODO: check chained configs with :
 Below example was run with config::
 
     robocop format -c AlignKeywordsSection.enabled=True -c AlignKeywordsSection.handle_too_long=compact_overflow -c AlignKeywordsSection.widths=24,28,20
diff --git a/docs/source/formatters/formatters_list/AlignSettingsSection.rst b/docs/source/formatters/formatters_list/AlignSettingsSection.rst
index 1d5fc93..ab744e3 100644
--- a/docs/source/formatters/formatters_list/AlignSettingsSection.rst
+++ b/docs/source/formatters/formatters_list/AlignSettingsSection.rst
@@ -36,7 +36,7 @@ Align up to columns
 -------------------
 
 You can configure how many columns should be aligned to the longest token in given column. The remaining columns
-will use fixed length separator length ``--spacecount``. By default only first two columns are aligned.
+will use fixed length separator length ``--space-count``. By default only first two columns are aligned.
 
 Example of how AlignSettingsSection transformer behaves with default configuration and multiple columns:
 
diff --git a/docs/source/formatters/formatters_list/AlignVariablesSection.rst b/docs/source/formatters/formatters_list/AlignVariablesSection.rst
index 3596cbf..f88c8ba 100644
--- a/docs/source/formatters/formatters_list/AlignVariablesSection.rst
+++ b/docs/source/formatters/formatters_list/AlignVariablesSection.rst
@@ -34,7 +34,7 @@ Align up to columns
 -------------------
 
 You can configure how many columns should be aligned to longest token in given column. The remaining columns
-will use fixed length separator length ``--spacecount``. By default only first two columns are aligned.
+will use fixed length separator length ``--space-count``. By default only first two columns are aligned.
 
 Example of how AlignVariablesSection transformer behaves with default configuration and multiple columns:
 
diff --git a/docs/source/formatters/formatters_list/NormalizeSeparators.rst b/docs/source/formatters/formatters_list/NormalizeSeparators.rst
index fa69f93..79675bf 100644
--- a/docs/source/formatters/formatters_list/NormalizeSeparators.rst
+++ b/docs/source/formatters/formatters_list/NormalizeSeparators.rst
@@ -9,7 +9,7 @@ Normalize separators and indents.
 .. include:: enabled_hint.txt
 
 All separators (pipes included) are converted to fixed length of 4 spaces (configurable via global option
-``--spacecount``). To separately configure the indentation, use ``--indent`` global option.
+``--space-count``). To separately configure the indentation, use ``--indent`` global option.
 
 .. note::
     There are formatters that also affect separator lengths - for example ``AlignSettingsSection``. ``NormalizeSeparators``
@@ -79,9 +79,9 @@ All separators (pipes included) are converted to fixed length of 4 spaces (confi
 Configure separator
 --------------------
 
-By configuring a global option ``spacecount``, you can change the default separator length::
+By configuring a global option ``space-count``, you can change the default separator length::
 
-    robocop format --spacecount 8
+    robocop format --space-count 8
 
 .. tab-set::
 
@@ -108,13 +108,13 @@ By configuring a global option ``spacecount``, you can change the default separa
 Indentation
 ------------
 
-By default, indentation is the same as ``spacecount`` value (default ``4`` spaces). To configure it, use ``--indent``::
+By default, indentation is the same as ``space-count`` value (default ``4`` spaces). To configure it, use ``--indent``::
 
     robocop format --indent 4
 
-Combine it with ``spacecount`` to set whitespace separately for indent and separators::
+Combine it with ``space-count`` to set whitespace separately for indent and separators::
 
-    robocop format --indent 4 --spacecount 2
+    robocop format --indent 4 --space-count 2
 
 .. tab-set::
 
diff --git a/docs/source/formatters/formatters_list/OrderSettings.rst b/docs/source/formatters/formatters_list/OrderSettings.rst
index b0cb911..ef7ee98 100644
--- a/docs/source/formatters/formatters_list/OrderSettings.rst
+++ b/docs/source/formatters/formatters_list/OrderSettings.rst
@@ -11,8 +11,6 @@ Order settings like ``[Arguments]``, ``[Setup]``, ``[Tags]`` inside Keywords and
 Test case settings ``[Documentation]``, ``[Tags]``, ``[Template]``, ``[Timeout]``, ``[Setup]`` are put before test case
 body and ``[Teardown]`` is moved to the end of test case.
 
-# FIXME: Does keyword actually support [Setup]?
-
 Keyword settings ``[Documentation]``, ``[Tags]``, ``[Timeout]``, ``[Arguments]``, ``[Setup]`` are put before keyword
 body and settings like ``[Teardown]``, ``[Return]`` are moved to the end of keyword.
 
diff --git a/docs/source/formatters/formatters_list/index.rst b/docs/source/formatters/formatters_list/index.rst
index 627bed2..4d9dfef 100644
--- a/docs/source/formatters/formatters_list/index.rst
+++ b/docs/source/formatters/formatters_list/index.rst
@@ -16,16 +16,15 @@ To see list of all formatters currently implemented in `robocop format` run:
 
 .. code:: shell
 
-    robocop format --list
+    robocop list formatters
 
 Formatters are sorted in the order they are run by default.
 
-# TODO
 To see description of the transformer run:
 
 .. code:: shell
 
-    robocop format --desc TRANSFORMER_NAME
+    robocop docs formatter_name
 
 See :ref:`configuring-formatters` to learn how formatters can be configured (including disabling and enabling formatters).
 
@@ -49,10 +48,10 @@ or:
 
    robocop format --select SplitTooLongLine --select ReplaceRunKeywordIf
 
-It will transform files according to internal order (in this example ``ReplaceRunKeywordIf`` is before
-``SplitTooLongLine``). To see order of the formatters run ``robocop format --list``.
+It will format files according to internal order (in this example ``ReplaceRunKeywordIf`` is before
+``SplitTooLongLine``). To see order of the formatters run ``robocop list formatters``.
 
-If you want to transform files using different transformer order you need to run formatters separately:
+If you want to format files using different formatter order you need to run formatters separately:
 
 .. code:: shell
 
diff --git a/docs/source/overview/formatter.rst b/docs/source/overview/formatter.rst
index 4b4ac57..1c5a2e7 100644
--- a/docs/source/overview/formatter.rst
+++ b/docs/source/overview/formatter.rst
@@ -77,12 +77,10 @@ See :ref:`configuration` for information how to configure `Robocop`.
 List of formatters
 -------------------
 
-# TODO
-
-To see list of formatters included with `Robocop` use ``list formatters``::
+To see list of formatters included with `Robocop` use ``robocop list formatters``::
 
     > robocop list formatters
-                  Transformers
+                   Formatters
     ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
     ┃ Name                       ┃ Enabled ┃
     ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
@@ -91,11 +89,15 @@ To see list of formatters included with `Robocop` use ``list formatters``::
     │ DiscardEmptySections       │ Yes     │
     │ MergeAndOrderSections      │ Yes     │
     │ RemoveEmptySettings        │ Yes     │
+    │ ReplaceEmptyValues         │ Yes     │
+    │ ReplaceWithVAR             │ No      │
     │ NormalizeAssignments       │ Yes     │
+    │ GenerateDocumentation      │ No      │
     │ OrderSettings              │ Yes     │
     │ OrderSettingsSection       │ Yes     │
     │ NormalizeTags              │ Yes     │
     │ OrderTags                  │ No      │
+    │ RenameVariables            │ No      │
     │ IndentNestedKeywords       │ No      │
     │ AlignSettingsSection       │ Yes     │
     │ AlignVariablesSection      │ Yes     │
@@ -114,17 +116,13 @@ To see list of formatters included with `Robocop` use ``list formatters``::
     │ ReplaceBreakContinue       │ Yes     │
     │ InlineIf                   │ Yes     │
     │ Translate                  │ No      │
+    │ NormalizeComments          │ Yes     │
     └────────────────────────────┴─────────┘
-
-    Formatters are listed in the order they are run by default. The status of the formatter will be displayed in the
-    different color if it is changed by the configuration.
     To see detailed docs run:
-        robotidy --desc formatter_nameF
-    or
-        robotidy --desc all
-
+        robocop docs formatter_name
     Non-default formatters needs to be selected explicitly with --select or configured with param `enabled=True`.
 
+FIXME
 Pass optional value ``enabled`` or ``disabled`` to filter out output by the status of the formatter::
 
     > robocop list formatters --filter disabled
@@ -144,34 +142,22 @@ Pass optional value ``enabled`` or ``disabled`` to filter out output by the stat
     └─────────────────────────┴─────────┘
     (...)
 
-The configuration is reflected in the output. For example combining ``--select`` (which only runs selected
-formatters) and ``enabled`` gives us::
-
-    > robocop list formatters --filter enabled --select DiscardEmptySections
-               Transformers
-    ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
-    ┃ Name                 ┃ Enabled ┃
-    ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
-    │ DiscardEmptySections │ Yes     │
-    └──────────────────────┴─────────┘
-    (...)
 
-# TODO: replacement
-You can display short documentation on particular transformer with ``--desc``::
+You can display short documentation on particular transformer with ``docs formatter``::
 
-    > robotidy --desc DiscardEmptySections
-    Transformer DiscardEmptySections:
+    > robocop docs DiscardEmptySections
+    Formatter DiscardEmptySections:
 
-        Remove empty sections.
-        Sections are considered empty if there are only empty lines inside.
-        You can remove sections with only comments by setting 'allow_only_comments' parameter to False:
+    Remove empty sections.
+    Sections are considered empty if there are only empty lines inside.
+    You can remove sections with only comments by setting 'allow_only_comments' parameter to False:
 
-            *** Variables ***
-            # this section will be removed with'alow_only_comments' parameter set to False
+        *** Variables ***
+        # this section will be removed with'alow_only_comments' parameter set to False
 
-        Supports global formatting params: '--start-line' and '--end-line'.
+    Supports global formatting params: '--start-line' and '--end-line'.
 
-        See https://robocop.readthedocs.io/en/stable/formatters/formatters_list/DiscardEmptySections.html for more examples.
+    See https://robocop.readthedocs.io/en/stable/formatters/formatters_list/DiscardEmptySections.html for more examples.
 
 Format selected lines
 ---------------------
diff --git a/src/robocop/config.py b/src/robocop/config.py
index 97c6757..a3b59b6 100644
--- a/src/robocop/config.py
+++ b/src/robocop/config.py
@@ -385,7 +385,6 @@ class FormatterConfig:
 
     def load_formatters(self):
         self._formatters = {}
-        allow_version_mismatch = False
         self.load_languages()
         for formatter in self.selected_formatters():
             for container in formatters.import_formatter(formatter, self.combined_configure, self.skip_config):
@@ -402,11 +401,10 @@ class FormatterConfig:
                     overwritten=container.name in self.select,
                     target_version=self.target_version,
                 ):
-                    container.enabled_by_default = enabled
+                    container.instance.ENABLED = enabled
                     self._formatters[container.name] = container.instance
-                elif allow_version_mismatch and self.allow_disabled:
+                elif self.allow_disabled:
                     container.instance.ENABLED = False
-                    container.enabled_by_default = False
                     self._formatters[container.name] = container.instance
                 container.instance.formatting_config = self.whitespace_config
                 container.instance.formatters = self.formatters
diff --git a/src/robocop/formatter/formatters/NormalizeSeparators.py b/src/robocop/formatter/formatters/NormalizeSeparators.py
index c46f6de..645372c 100644
--- a/src/robocop/formatter/formatters/NormalizeSeparators.py
+++ b/src/robocop/formatter/formatters/NormalizeSeparators.py
@@ -17,7 +17,7 @@ class NormalizeSeparators(Formatter):
     Normalize separators and indents.
 
     All separators (pipes included) are converted to fixed length of 4 spaces (configurable via global argument
-    ``--spacecount``).
+    ``--space-count``).
 
     To not format documentation configure ``skip_documentation`` to ``True``.
     """
diff --git a/src/robocop/linter/rules/__init__.py b/src/robocop/linter/rules/__init__.py
index 0dcc3cc..0d40344 100644
--- a/src/robocop/linter/rules/__init__.py
+++ b/src/robocop/linter/rules/__init__.py
@@ -159,10 +159,16 @@ class RuleFilter(str, Enum):
     STYLE_GUIDE = "STYLE_GUIDE"
 
 
+def rule_matches_pattern(rule: Rule, pattern: str | Pattern) -> bool:
+    if isinstance(pattern, str):
+        return pattern in (rule.name, rule.rule_id)
+    return pattern.match(rule.name) or pattern.match(rule.rule_id)
+
+
 def filter_rules_by_pattern(rules: dict[str, Rule], pattern: Pattern) -> list[Rule]:
     """Return sorted list of Rules from rules dictionary, filtered out by pattern."""
     return rules_sorted_by_id(
-        {rule.rule_id: rule for rule in rules.values() if rule.matches_pattern(pattern) and not rule.deprecated}
+        {rule.rule_id: rule for rule in rules.values() if rule_matches_pattern(rule, pattern) and not rule.deprecated}
     )
 
 
diff --git a/src/robocop/run.py b/src/robocop/run.py
index 12986ea..980ce6c 100644
--- a/src/robocop/run.py
+++ b/src/robocop/run.py
@@ -546,7 +546,7 @@ def list_rules(
     """
     # TODO: rich support (colorized enabled, severity etc)
     console = Console(soft_wrap=True)
-    linter_config = config.LinterConfig(  # set to None's to not override
+    linter_config = config.LinterConfig(  # set to None to not override
         configure=None,
         select=None,
         ignore=None,
@@ -624,7 +624,6 @@ def list_formatters(
     filter_category: Annotated[
         RuleFilter, typer.Option("--filter", case_sensitive=False, help="Filter formatters by category.")
     ] = RuleFilter.ALL,
-    filter_pattern: Annotated[Optional[str], typer.Option("--pattern", help="Filter formatters by pattern")] = None,
     target_version: Annotated[
         config.TargetVersion,
         typer.Option(
@@ -634,9 +633,56 @@ def list_formatters(
     ] = None,
 ) -> None:
     """List available formatters."""
-    # We will need ConfigManager later for listing based on configuration
-    # FIXME
-    raise NotImplementedError("Command not yet implemented.")
+    from rich.table import Table
+
+    console = Console(soft_wrap=True)
+    formatter_config = config.FormatterConfig(
+        select=None,
+        custom_formatters=None,
+        force_order=None,
+        whitespace_config=config.WhitespaceConfig(),
+        skip_config=config.SkipConfig(),
+        configure=None,
+        overwrite=None,
+        output=None,
+        diff=None,
+        check=None,
+        start_line=None,
+        end_line=None,
+        reruns=None,
+        allow_disabled=True,
+    )
+    overwrite_config = config.Config(
+        linter=None,
+        formatter=formatter_config,
+        file_filters=None,
+        language=None,
+        verbose=None,
+        target_version=target_version,
+    )
+    config_manager = config.ConfigManager(overwrite_config=overwrite_config)
+    default_config = config_manager.default_config
+    if filter_category == filter_category.ALL:
+        formatters = list(default_config.formatter.formatters.values())
+    elif filter_category == filter_category.ENABLED:
+        formatters = [formatter for formatter in default_config.formatter.formatters.values() if formatter.ENABLED]
+    elif filter_category == filter_category.DISABLED:
+        formatters = [formatter for formatter in default_config.formatter.formatters.values() if not formatter.ENABLED]
+    else:
+        raise ValueError(f"Unrecognized rule category '{filter_category}'")
+    table = Table(title="Formatters", header_style="bold red")
+    table.add_column("Name", justify="left", no_wrap=True)
+    table.add_column("Enabled")
+    for formatter in formatters:
+        decorated_enable = "Yes" if formatter.ENABLED else "No"
+        table.add_row(formatter.__class__.__name__, decorated_enable)
+    console.print(table)
+    console.print(
+        "To see detailed docs run:\n"
+        "    [bold]robocop docs [blue]formatter_name[/][/]\n"
+        "Non-default formatters needs to be selected explicitly with [bold cyan]--select[/] or "
+        "configured with param `enabled=True`.\n"
+    )
 
 
 @app.command("docs")
@@ -661,7 +707,11 @@ def print_resource_documentation(name: Annotated[str, typer.Argument(help="Rule
     formatter_config = config.FormatterConfig()
     if name in formatter_config.formatters:
         docs = textwrap.dedent(formatter_config.formatters[name].__doc__)
+        console.print(f"Formatter [bold]{name}[/bold]:")
         console.print(docs)
+        console.print(
+            f"See https://robocop.readthedocs.io/en/stable/formatters/formatters_list/{name}.html for more information."
+        )
     else:
         console.print(f"There is no rule, formatter or a report with a '{name}' name.")
         raise typer.Exit(code=2)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 8b98cd5..0633496 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -42,3 +42,34 @@ def test_invalid_threshold():
     result = runner.invoke(app, ["check", "--threshold", "unknown"])
     assert result.exit_code == 2
     assert result.stdout == "ConfigurationError: Invalid severity value 'unknown'. Choose one from: I, W, E.\n"
+
+
+class TestListFormatters:
+    def test_list_formatters_default(self):
+        runner = CliRunner()
+        result = runner.invoke(app, ["list", "formatters"])
+        assert result.exit_code == 0
+        assert "NormalizeNewLines" in result.stdout
+        assert "Translate" in result.stdout
+
+    def test_list_enabled(self):
+        runner = CliRunner()
+        result = runner.invoke(app, ["list", "formatters", "--filter", "ENABLED"])
+        assert result.exit_code == 0
+        assert "NormalizeNewLines" in result.stdout
+        assert "ReplaceReturns" in result.stdout
+        assert "Translate" not in result.stdout
+
+    def test_list_disabled(self):
+        runner = CliRunner()
+        result = runner.invoke(app, ["list", "formatters", "--filter", "DISABLED"])
+        assert result.exit_code == 0
+        assert "NormalizeNewLines" not in result.stdout
+        assert "Translate" in result.stdout
+
+    def test_target_version(self):
+        runner = CliRunner()
+        result = runner.invoke(app, ["list", "formatters", "--filter", "ENABLED", "--target-version", "4"])
+        assert result.exit_code == 0
+        assert "NormalizeNewLines" in result.stdout
+        assert "ReplaceReturns" not in result.stdout