diff --git a/tools/pylint-extensions/azure-pylint-guidelines-checker/README.md b/tools/pylint-extensions/azure-pylint-guidelines-checker/README.md index a4d7b444323..6cbf0d4ccee 100644 --- a/tools/pylint-extensions/azure-pylint-guidelines-checker/README.md +++ b/tools/pylint-extensions/azure-pylint-guidelines-checker/README.md @@ -99,4 +99,5 @@ In the case of a false positive, use the disable command to remove the pylint er | invalid-use-of-overload | Do not mix async and synchronous overloads | pylint:disable=invalid-use-of-overload | No Link. | | do-not-hardcode-connection-verify | Do not hardcode a boolean value to connection_verify | pylint:disable=do-not-hardcode-connection-verify | No LInk. | | do-not-log-exceptions | Do not log exceptions in levels other than debug, otherwise it can reveal sensitive information | pylint:disable=do-not-log-exceptions | [link](https://azure.github.io/azure-sdk/python_implementation.html#python-logging-sensitive-info) | -| unapproved-client-method-name-prefix | Clients should use preferred verbs for method names | pylint:disable=unapproved-client-method-name-prefix | [link](https://azure.github.io/azure-sdk/python_design.html#naming) | \ No newline at end of file +| unapproved-client-method-name-prefix | Clients should use preferred verbs for method names | pylint:disable=unapproved-client-method-name-prefix | [link](https://azure.github.io/azure-sdk/python_design.html#naming) | +| do-not-hardcode-dedent | Sphinx will automatically dedent examples. | pylint:disable=do-not-hardcode-dedent | No Link. | \ No newline at end of file diff --git a/tools/pylint-extensions/azure-pylint-guidelines-checker/pylint_guidelines_checker.py b/tools/pylint-extensions/azure-pylint-guidelines-checker/pylint_guidelines_checker.py index ccc024a91b0..8f46b4b8d59 100644 --- a/tools/pylint-extensions/azure-pylint-guidelines-checker/pylint_guidelines_checker.py +++ b/tools/pylint-extensions/azure-pylint-guidelines-checker/pylint_guidelines_checker.py @@ -3079,6 +3079,80 @@ def visit_annassign(self, node): except: pass +class DoNotDedentDocstring(BaseChecker): + + """Rule to check that developers do not hardcode `dedent` in their docstring. Sphinx will handle this automatically.""" + + name = "do-not-hardcode-dedent" + priority = -1 + msgs = { + "C4768": ( + "Do not hardcode dedent value in docstring", + "do-not-hardcode-dedent", + "Do not hardcode dedent value in docstring. It's up to sphinx to handle this automatically", + ), + } + + def __init__(self, linter=None): + super(DoNotDedentDocstring, self).__init__(linter) + + def check_for_dedent(self, node): + """Parse the docstring for a dedent. + If found, checks that the dedent does not have a value set. + + :param node: ast.ClassDef or ast.FunctionDef + :return: None + """ + + try: + # not every class/method will have a docstring so don't crash here, just return + # don't fail if there is no dedent in the docstring, be lenient + if ( + node.doc_node.value.find(":dedent") != -1 + ): + dedent_value = node.doc_node.value.split(":dedent:")[1].split("\n")[0].strip() + try: + int(dedent_value) + self.add_message( + "do-not-hardcode-dedent", + node=node, + confidence=None, + ) + except: + pass + except Exception: + return + + def visit_classdef(self, node): + """Visits every class docstring. + + :param node: ast.ClassDef + :return: None + """ + try: + for func in node.body: + if isinstance(func, astroid.FunctionDef) and func.name == "__init__": + self.check_for_dedent(node) + except Exception: + logger.debug("Pylint custom checker failed to check docstrings.") + pass + + def visit_functiondef(self, node): + """Visits every method docstring. + + :param node: ast.FunctionDef + :return: None + """ + try: + if node.name == "__init__": + return + self.check_for_dedent(node) + except Exception: + logger.debug("Pylint custom checker failed to check docstrings.") + pass + + # this line makes it work for async functions + visit_asyncfunctiondef = visit_functiondef # if a linter is registered in this function then it will be checked with pylint def register(linter): @@ -3117,8 +3191,8 @@ def register(linter): linter.register_checker(DoNotLogErrorsEndUpRaising(linter)) linter.register_checker(InvalidUseOfOverload(linter)) linter.register_checker(DoNotLogExceptions(linter)) - linter.register_checker(DoNotHardcodeConnectionVerify(linter)) + linter.register_checker(DoNotDedentDocstring(linter)) # disabled by default, use pylint --enable=check-docstrings if you want to use it linter.register_checker(CheckDocstringParameters(linter)) diff --git a/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_files/dedent_failure.py b/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_files/dedent_failure.py new file mode 100644 index 00000000000..70f0582e573 --- /dev/null +++ b/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_files/dedent_failure.py @@ -0,0 +1,64 @@ +# test_ignores_correct_dedent_in_function +def function_foo(x, y, z): + """docstring + .. admonition:: Example: + + .. literalinclude:: ../samples/sample_authentication.py + :start-after: [START auth_from_connection_string] + :end-before: [END auth_from_connection_string] + :language: python + :dedent: + :caption: Authenticate with a connection string + """ + pass + + +# test_failure_dedent_in_function +def function_foo1(x, y, z): + """docstring + .. admonition:: Example: + This is Example content. + Should support multi-line. + Can also include file: + + .. literalinclude:: ../samples/sample_authentication.py + :start-after: [START auth_from_connection_string] + :end-before: [END auth_from_connection_string] + :language: python + :dedent: 8 + """ + + +# test_ignores_correct_dedent_in_class +class SomeClient(object): + """docstring + .. admonition:: Example: + .. literalinclude:: ../samples/sample_authentication.py + :start-after: [START auth_from_connection_string] + :end-before: [END auth_from_connection_string] + :language: python + :dedent: + :caption: Authenticate with a connection string + """ + + def __init__(self): + pass + + +# test_failure_dedent_in_class +class Some1Client(): # @ + """docstring + .. admonition:: Example: + This is Example content. + Should support multi-line. + Can also include file: + + .. literalinclude:: ../samples/sample_authentication.py + :start-after: [START auth_from_connection_string] + :end-before: [END auth_from_connection_string] + :language: python + :dedent: 8 + """ + + def __init__(self): + pass \ No newline at end of file diff --git a/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_pylint_custom_plugins.py b/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_pylint_custom_plugins.py index b1d95b7f315..df81001f461 100644 --- a/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_pylint_custom_plugins.py +++ b/tools/pylint-extensions/azure-pylint-guidelines-checker/tests/test_pylint_custom_plugins.py @@ -3901,3 +3901,55 @@ def test_invalid_connection_verify(self): self.checker.visit_annassign(annotated_assignment) self.checker.visit_annassign(annotated_self_assignment) + +class TestDedent(pylint.testutils.CheckerTestCase): + """Test that we are checking the dedent is not set in the docstring""" + + CHECKER_CLASS = checker.DoNotDedentDocstring + + @pytest.fixture(scope="class") + def setup(self): + file = open( + os.path.join(TEST_FOLDER, "test_files", "dedent_failure.py") + ) + node = astroid.parse(file.read()) + file.close() + return node + + def test_ignores_correct_dedent_in_function(self, setup): + function_node = setup.body[0] + with self.assertNoMessages(): + self.checker.visit_functiondef(function_node) + + def test_bad_dedent_in_function(self, setup): + function_node = setup.body[1] + with self.assertAddsMessages( + pylint.testutils.MessageTest( + msg_id="do-not-hardcode-dedent", + line=17, + node=function_node, + col_offset=0, + end_line=17, + end_col_offset=17, + ) + ): + self.checker.visit_functiondef(function_node) + + def test_ignores_correct_dedent_in_class(self, setup): + function_node = setup.body[2] + with self.assertNoMessages(): + self.checker.visit_classdef(function_node) + + def test_bad_dedent_in_class(self, setup): + function_node = setup.body[3] + with self.assertAddsMessages( + pylint.testutils.MessageTest( + msg_id="do-not-hardcode-dedent", + line=49, + node=function_node, + col_offset=0, + end_line=49, + end_col_offset=17, + ) + ): + self.checker.visit_classdef(function_node) \ No newline at end of file