Repository: MarketSquare/robotframework-robocop (language: python)

Issue:
Disallow usage of specific KW
Hi, Could You add rule where we can specify a Keyword which should not be used in RF code ?


Discussion / hints:
Yes, it can be implemented as community rule. It would reuse most of the code from the ``sleep-keyword-used`` with the possiblity to configure keyword name and optionally library/resource name (if used as library.keyword)
I need to think how to handle multiple keywords - most likely allow to define multiple keywords (ie rule-name:keywords:keyword1,keyword2) .
I have implemented rule in the linked PR. Can you @juszynsk check the PR tests and see if it will cover your needs? 

Fix patch:
diff --git a/docs/releasenotes/unreleased/rules.1.rst b/docs/releasenotes/unreleased/rules.1.rst
new file mode 100644
index 000000000..1ed4e8970
--- /dev/null
+++ b/docs/releasenotes/unreleased/rules.1.rst
@@ -0,0 +1,23 @@
+Community rule: not-allowed-keyword (#1067)
+-------------------------------------------
+
+New community (optional) rule W10002 ``not-allowed-keyword``. You can use this rule to find keywords that should not
+be used in your project.
+
+For example with following configuration::
+
+    > > robocop -i not-allowed-keyword -c not-allowed-keyword:keywords:click_using_javascript,click_with_sleep
+
+It will find and raise issues in the following code::
+
+    *** Test Cases ***
+    Test with obsolete keyword
+        [Setup]    Click With Sleep    1 min  # Robocop will report not allowed keyword
+        Test Step
+
+
+    *** Keywords ***
+    Keyword With Obsolete Implementation
+        [Arguments]    ${locator}
+        Click Using Javascript    ${locator}  # Robocop will report not allowed keyword
+
diff --git a/robocop/checkers/community_rules/keywords.py b/robocop/checkers/community_rules/keywords.py
index 965a8496e..8c04bb1cd 100644
--- a/robocop/checkers/community_rules/keywords.py
+++ b/robocop/checkers/community_rules/keywords.py
@@ -1,12 +1,22 @@
+from typing import Set
+
 from robot.api import Token
 from robot.utils.robottime import timestr_to_secs
 
 from robocop.checkers import VisitorChecker
 from robocop.rules import Rule, RuleParam, RuleSeverity
 from robocop.utils.misc import normalize_robot_name
+from robocop.utils.run_keywords import iterate_keyword_names
 
 RULE_CATEGORY_ID = "00"
 
+
+def comma_separated_list(value: str) -> Set[str]:
+    if value is None:
+        return set()
+    return {normalize_robot_name(kw) for kw in value.split(",")}
+
+
 rules = {
     "10001": Rule(
         RuleParam(
@@ -43,7 +53,41 @@
             robocop -c sleep-keyword-used:max_time:1min .
 
         """,
-    )
+    ),
+    "10002": Rule(
+        RuleParam(
+            name="keywords",
+            default=None,
+            converter=comma_separated_list,
+            desc="Comma separated list of not allowed keywords",
+        ),
+        rule_id="10002",
+        name="not-allowed-keyword",
+        msg="Keyword '{{ keyword }}' is not allowed",
+        severity=RuleSeverity.WARNING,
+        added_in_version="5.1.0",
+        enabled=False,
+        docs="""
+        Reports usage of not allowed keywords.
+        
+        Configure which keywords should be reported by using ``keywords`` parameter.
+        Keyword names are normalized to match Robot Framework search behaviour (lower case, removed whitespace and
+        underscores). 
+
+        For example::
+        
+            > robocop -i not-allowed-keyword -c not-allowed-keyword:keywords:click_using_javascript
+
+            *** Keywords ***
+            Keyword With Obsolete Implementation
+                [Arguments]    ${locator}
+                Click Using Javascript    ${locator}  # Robocop will report not allowed keyword
+
+        If keyword call contains possible library name (ie. Library.Keyword Name), Robocop checks if it matches
+        the not allowed keywords and if not, it will remove library part and check again.
+
+        """,
+    ),
 }
 
 
@@ -89,3 +133,48 @@ def visit_KeywordCall(self, node):  # noqa
             col=name_token.col_offset + 1,
             end_col=name_token.end_col_offset + 1,
         )
+
+
+class NotAllowedKeyword(VisitorChecker):
+    reports = ("not-allowed-keyword",)
+
+    def check_keyword_naming_with_subkeywords(self, node, name_token_type):
+        for keyword in iterate_keyword_names(node, name_token_type):
+            self.check_keyword_naming(keyword.value, keyword)
+
+    def check_keyword_naming(self, name: str, keyword):
+        if not name:
+            return
+        not_allowed = self.param("not-allowed-keyword", "keywords")
+        normalized_name = normalize_robot_name(name)
+        if normalized_name not in not_allowed:
+            if "." not in normalized_name:
+                return
+            # handle possible library names (builtin.log)
+            normalized_name = normalized_name.split(".")[-1]
+            if normalized_name not in not_allowed:
+                return
+        self.report(
+            "not-allowed-keyword",
+            keyword=name,
+            node=keyword,
+            col=keyword.col_offset + 1,
+            end_col=keyword.end_col_offset + 1,
+        )
+
+    def visit_Setup(self, node):  # noqa
+        self.check_keyword_naming_with_subkeywords(node, Token.NAME)
+
+    visit_TestTeardown = visit_SuiteTeardown = visit_Teardown = visit_TestSetup = visit_SuiteSetup = visit_Setup
+
+    def visit_Template(self, node):  # noqa
+        # allow / disallow param
+        if node.value:
+            name_token = node.get_token(Token.NAME)
+            self.check_keyword_naming(node.value, name_token)
+        self.generic_visit(node)
+
+    visit_TestTemplate = visit_Template
+
+    def visit_KeywordCall(self, node):  # noqa
+        self.check_keyword_naming_with_subkeywords(node, Token.KEYWORD)


Test patch:
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/__init__.py b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output.txt b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output.txt
new file mode 100644
index 000000000..750701607
--- /dev/null
+++ b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output.txt
@@ -0,0 +1,14 @@
+test.robot:2:16:2:27 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:4:15:4:26 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:5:18:5:39 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:15:5:15:16 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:16:25:16:35 [W] 10002 Keyword 'Notallowed' is not allowed
+test.robot:18:9:18:30 [W] 10002 Keyword 'Not_allowed With Args' is not allowed
+test.robot:23:9:23:20 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:28:12:28:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:30:12:30:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:31:19:31:40 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:34:19:34:30 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:38:5:38:33 [W] 10002 Keyword 'Library.Not Allowed With Lib' is not allowed
+test.robot:39:5:39:32 [W] 10002 Keyword 'Library2.Nested.Not Allowed' is not allowed
+test.robot:44:5:44:16 [W] 10002 Keyword 'Not Allowed' is not allowed
\ No newline at end of file
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf3.txt b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf3.txt
new file mode 100644
index 000000000..270a97003
--- /dev/null
+++ b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf3.txt
@@ -0,0 +1,13 @@
+test.robot:2:16:2:27 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:4:15:4:26 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:5:18:5:39 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:15:5:15:16 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:18:9:18:30 [W] 10002 Keyword 'Not_allowed With Args' is not allowed
+test.robot:23:9:23:20 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:28:12:28:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:30:12:30:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:31:19:31:40 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:34:19:34:30 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:38:5:38:33 [W] 10002 Keyword 'Library.Not Allowed With Lib' is not allowed
+test.robot:39:5:39:32 [W] 10002 Keyword 'Library2.Nested.Not Allowed' is not allowed
+test.robot:44:5:44:16 [W] 10002 Keyword 'Not Allowed' is not allowed
\ No newline at end of file
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf4.txt b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf4.txt
new file mode 100644
index 000000000..270a97003
--- /dev/null
+++ b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/expected_output_rf4.txt
@@ -0,0 +1,13 @@
+test.robot:2:16:2:27 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:4:15:4:26 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:5:18:5:39 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:15:5:15:16 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:18:9:18:30 [W] 10002 Keyword 'Not_allowed With Args' is not allowed
+test.robot:23:9:23:20 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:28:12:28:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:30:12:30:23 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:31:19:31:40 [W] 10002 Keyword 'Not Allowed With Args' is not allowed
+test.robot:34:19:34:30 [W] 10002 Keyword 'Not Allowed' is not allowed
+test.robot:38:5:38:33 [W] 10002 Keyword 'Library.Not Allowed With Lib' is not allowed
+test.robot:39:5:39:32 [W] 10002 Keyword 'Library2.Nested.Not Allowed' is not allowed
+test.robot:44:5:44:16 [W] 10002 Keyword 'Not Allowed' is not allowed
\ No newline at end of file
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test.robot b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test.robot
new file mode 100644
index 000000000..ab17e651f
--- /dev/null
+++ b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test.robot
@@ -0,0 +1,46 @@
+*** Settings ***
+Suite Setup    Not Allowed
+Suite Teardown
+Test Setup    Not Allowed
+Test Teardown    Not Allowed With Args
+...    ${arg}
+
+
+*** Test Cases ***
+Test with allowed keywords
+    Allowed
+    Allowed With Args    ${arg}
+
+Test with not allowed keywords
+    Not Allowed
+    IF    $condition    Notallowed
+    FOR    ${var}    IN RANGE    10
+        Not_allowed With Args    ${arg}
+        ...    ${arg}
+    END
+    IF    True
+        Log    statement
+        Not Allowed
+    END
+
+Test with settings
+    [Setup]    Run Keywords
+    ...    Not Allowed
+    ...    AND
+    ...    Not Allowed
+    [Teardown]    Not Allowed With Args    ${arg}
+
+Test with template
+    [Template]    Not Allowed
+
+Test with library
+    Not Allowed With Lib  # should be ignored
+    Library.Not Allowed With Lib  # should be reported
+    Library2.Nested.Not Allowed  # should be reported
+
+
+*** Keywords ***
+Keyword with not allowed
+    Not Allowed
+
+# while and stuff?
diff --git a/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test_rule.py b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test_rule.py
new file mode 100644
index 000000000..8f09685ae
--- /dev/null
+++ b/tests/atest/rules/community_rules/keywords/not_allowed_keyword/test_rule.py
@@ -0,0 +1,30 @@
+from tests.atest.utils import RuleAcceptance
+
+
+class TestRuleAcceptance(RuleAcceptance):
+    def test_rule(self):
+        self.check_rule(
+            src_files=["test.robot"],
+            expected_file="expected_output.txt",
+            config="-c not-allowed-keyword:keywords:NotAllowed,Not_AllowedWithArgs,library.not_allowed_with_lib",
+            issue_format="end_col",
+            target_version=">=5",
+        )
+
+    def test_rule_rf3(self):
+        self.check_rule(
+            src_files=["test.robot"],
+            expected_file="expected_output_rf3.txt",
+            config="-c not-allowed-keyword:keywords:NotAllowed,Not_AllowedWithArgs,library.not_allowed_with_lib",
+            issue_format="end_col",
+            target_version="==3.*",
+        )
+
+    def test_rule_rf4(self):
+        self.check_rule(
+            src_files=["test.robot"],
+            expected_file="expected_output_rf4.txt",
+            config="-c not-allowed-keyword:keywords:NotAllowed,Not_AllowedWithArgs,library.not_allowed_with_lib",
+            issue_format="end_col",
+            target_version="==4.*",
+        )