Repository: MarketSquare/robotframework-robocop (python)

Issue:
Rule version specifier should allow for multiple versions
If the rule is only valid for set of versions, ie rule was introduced in RF 5 but will not make sense after RF 8:

```
version=">=5;<8"
```

We can use ";" in version specifier to separate different versions. Rule will be only enabled if it matches all conditions from the list.

Pull request description:
Allow for multiple RF versions in rule rf version specifier
Closes #1026 

Patch:
diff --git a/docs/external_rules.rst b/docs/external_rules.rst
index 1a62b25..c62d624 100644
--- a/docs/external_rules.rst
+++ b/docs/external_rules.rst
@@ -206,6 +206,18 @@ You can enable (or disable) your rule for particular Robot Framework version. Ad
 
 In this case rule "external-rule" will be enabled only for Robot Framework versions equal to 5.0 or higher.
 
+To enable rule only for given range of versions, use ``;`` as a delimiter::
+
+    rules = {
+        "1105": Rule(
+            rule_id="1105",
+            name="range-5-and-6",
+            msg="Rule that is only enabled for RF version higher than 5 and lower or equal to 6",
+            severity=RuleSeverity.INFO,
+            version=">5;<=6",
+        ),
+    }
+
 It is also possible to adjust behavior of your checker depending on the Robot Framework version:
 
 ..  code-block:: python
diff --git a/docs/releasenotes/unreleased/other.4.rst b/docs/releasenotes/unreleased/other.4.rst
new file mode 100644
index 0000000..21f66f2
--- /dev/null
+++ b/docs/releasenotes/unreleased/other.4.rst
@@ -0,0 +1,15 @@
+Rule version matching allows for multiple Robot Framework versions (#1026)
+--------------------------------------------------------------------------
+
+When defining a rule it was possible to define Robot Framework version for which rule was enabled. Is is now also
+possible to define range of versions using ``;`` as separator::
+
+    rules = {
+        "1105": Rule(
+            rule_id="1105",
+            name="range-5-and-6",
+            msg="Rule that is only enabled for RF version higher than 5 and lower or equal to 6",
+            severity=RuleSeverity.INFO,
+            version=">5;<=6",
+        ),
+    }
diff --git a/robocop/rules.py b/robocop/rules.py
index f8f5cf5..7344f77 100644
--- a/robocop/rules.py
+++ b/robocop/rules.py
@@ -381,7 +381,7 @@ class Rule:
     def supported_in_rf_version(version: str) -> bool:
         if not version:
             return True
-        return ROBOT_VERSION in VersionSpecifier(version)
+        return all(ROBOT_VERSION in VersionSpecifier(condition) for condition in version.split(";"))
 
     @staticmethod
     def get_template(msg: str) -> Optional[Template]:
diff --git a/robocop/utils/version_matching.py b/robocop/utils/version_matching.py
index e5ddb53..b55494a 100644
--- a/robocop/utils/version_matching.py
+++ b/robocop/utils/version_matching.py
@@ -282,4 +282,4 @@ class VersionSpecifier:
         return prospective < Version(spec_str)
 
     def _compare_greater_than(self, prospective, spec_str: str) -> bool:
-        return prospective <= Version(spec_str)
+        return prospective > Version(spec_str)


Test patch:
diff --git a/tests/test_data/ext_rules/version_matching/rules_with_version_limits.py b/tests/test_data/ext_rules/version_matching/rules_with_version_limits.py
new file mode 100644
index 0000000..0a9bd1e
--- /dev/null
+++ b/tests/test_data/ext_rules/version_matching/rules_with_version_limits.py
@@ -0,0 +1,23 @@
+from robocop.checkers import VisitorChecker
+from robocop.rules import Rule, RuleSeverity
+
+rules = {
+    "1101": Rule(rule_id="1101", name="no-version", msg="No version", severity=RuleSeverity.INFO),
+    "1102": Rule(rule_id="1102", name="lower-than-5", msg="Lower than", severity=RuleSeverity.INFO, version="<5"),
+    "1103": Rule(
+        rule_id="1103",
+        name="higher-or-equal-than-5",
+        msg="Higher or equal than",
+        severity=RuleSeverity.INFO,
+        version=">=5",
+    ),
+    "1104": Rule(
+        rule_id="1104", name="range-5-and-6", msg="Version range", severity=RuleSeverity.INFO, version=">5;<=6"
+    ),
+}
+
+
+class EmptyChecker(VisitorChecker):
+    """Checker for keyword calls after [Return] statement."""
+
+    reports = ("no-version", "lower-than-5", "higher-or-equal-than-5", "range-5-and-6")
diff --git a/tests/utest/test_external_rules.py b/tests/utest/test_external_rules.py
index 1595b5f..2957698 100644
--- a/tests/utest/test_external_rules.py
+++ b/tests/utest/test_external_rules.py
@@ -8,6 +8,7 @@ import pytest
 
 import robocop.exceptions
 from robocop.config import Config
+from robocop.utils.version_matching import Version
 
 TEST_DATA = Path(__file__).parent.parent / "test_data" / "ext_rules"
 EXT_MODULE = str(TEST_DATA / "ext_rule_module")
@@ -214,3 +215,27 @@ class TestDeprecatedRules:
         out, _ = capsys.readouterr()
         assert "Rule W1103 deprecated-no-implementation is deprecated. Remove it from your configuration.\n" in out
         assert "Rule W1102 deprecated is deprecated. Remove it from your configuration.\n"
+
+
+class TestVersionMatching:
+    @pytest.mark.parametrize(
+        "rf_version, rules_status",
+        [
+            (
+                "4.0",
+                {"no-version": True, "lower-than-5": True, "higher-or-equal-than-5": False, "range-5-and-6": False},
+            ),
+            (
+                "5.0",
+                {"no-version": True, "lower-than-5": False, "higher-or-equal-than-5": True, "range-5-and-6": False},
+            ),
+            ("6.0", {"no-version": True, "lower-than-5": False, "higher-or-equal-than-5": True, "range-5-and-6": True}),
+        ],
+    )
+    def test_no_version_always_enabled(self, robocop_pre_load, rf_version, rules_status):
+        robocop_pre_load.config.ext_rules = {str(TEST_DATA / "version_matching" / "rules_with_version_limits.py")}
+
+        with patch("robocop.rules.ROBOT_VERSION", Version(rf_version)):
+            robocop_pre_load.reload_config()
+        actual_status = {rule: robocop_pre_load.rules[rule].enabled for rule in rules_status}
+        assert actual_status == rules_status