Repository: warpnet/salt-lint
Pull request #249 (2021-05-01 11:22:26)

feat: add frequent typo-catching rules
As Salt is unable to catch some frequent typos there are few checks:
- "onchange" (must be "onchanges")
- "requires" (must be "require")
- "content" (must be "contents")

jay7x: I put those rules as 'formatting' though we may have 'typos' category maybe..
jbouter: :wave: Hi @jay7x! Thanks for your PR! We are currently a bit short on time, but we'll try to look at your code soon.
jay7x: Ah! I see.. I missed that BAD_XXX_LINE & GOOD_XXX_LINE are different for every test somehow :-D
jbouter: Approved on my end, but I'd like a review by @roaldnefs as well. 
jay7x: Any chance for this PR to be merged or declined soon? :-D

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 617b747..d5009b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ All notable changes in **salt-lint** are documented below.
 ### Deprecated
 - Drop Python 2.7 support ((#239)[https://github.com/warpnet/salt-lint/pull/239]).
 
+### Added
+- Rule 216, 217 and 218 for catching common typographical errors ((#249)[https://github.com/warpnet/salt-lint/pull/249]).
+
 ## [0.5.2] (2021-01-29)
 ### Fixed
 - Append the contents of the `CHANGELOG.md` file to the long description of the package instead of the duplicate `README.md` contents ([#234](https://github.com/warpnet/salt-lint/pull/234)).
diff --git a/saltlint/linter/rule.py b/saltlint/linter/rule.py
index 587e83f..1d75f23 100644
--- a/saltlint/linter/rule.py
+++ b/saltlint/linter/rule.py
@@ -154,3 +154,13 @@ class DeprecationRule(Rule):
 
     def match(self, file, line):
         return self.regex.search(line)
+
+
+class TypographicalErrorRule(Rule):
+    """Base rule for catching common typographical errors."""
+    severity = 'LOW'
+    languages = [LANGUAGE_SLS]
+    tags = ['formatting']
+
+    def match(self, file, line):
+        return self.regex.search(line)
diff --git a/saltlint/rules/TypoContentsRule.py b/saltlint/rules/TypoContentsRule.py
new file mode 100644
index 0000000..52c3e25
--- /dev/null
+++ b/saltlint/rules/TypoContentsRule.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2016 Will Thames and contributors
+# Copyright (c) 2018 Ansible Project
+# Modified work Copyright (c) 2021 Warpnet B.V.
+# Modified work Copyright (c) 2021 Yury Bushmelev
+
+import re
+from saltlint.linter.rule import TypographicalErrorRule
+
+
+class TypoContentsRule(TypographicalErrorRule):
+    id = '218'
+    shortdesc = '"content" looks like a typo. Did you mean "contents"?'
+    description = '"content" looks like a typo. Did you mean "contents"?'
+    version_added = 'develop'
+    regex = re.compile(r"^\s+- content(|_pillar|_grains|_newline|_delimiter):")
diff --git a/saltlint/rules/TypoOnchangesRule.py b/saltlint/rules/TypoOnchangesRule.py
new file mode 100644
index 0000000..e49afe9
--- /dev/null
+++ b/saltlint/rules/TypoOnchangesRule.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2016 Will Thames and contributors
+# Copyright (c) 2018 Ansible Project
+# Modified work Copyright (c) 2021 Warpnet B.V.
+# Modified work Copyright (c) 2021 Yury Bushmelev
+
+import re
+from saltlint.linter.rule import TypographicalErrorRule
+
+
+class TypoOnchangesRule(TypographicalErrorRule):
+    id = '216'
+    shortdesc = '"onchange" looks like a typo. Did you mean "onchanges"?'
+    description = '"onchange" looks like a typo. Did you mean "onchanges"?'
+    version_added = 'develop'
+    regex = re.compile(r"^\s+- onchange(|_in|_any):")
diff --git a/saltlint/rules/TypoRequireRule.py b/saltlint/rules/TypoRequireRule.py
new file mode 100644
index 0000000..13440ec
--- /dev/null
+++ b/saltlint/rules/TypoRequireRule.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2016 Will Thames and contributors
+# Copyright (c) 2018 Ansible Project
+# Modified work Copyright (c) 2021 Warpnet B.V.
+# Modified work Copyright (c) 2021 Yury Bushmelev
+
+import re
+from saltlint.linter.rule import TypographicalErrorRule
+
+
+class TypoRequireRule(TypographicalErrorRule):
+    id = '217'
+    shortdesc = '"requires" looks like a typo. Did you mean "require"?'
+    description = '"requires" looks like a typo. Did you mean "require"?'
+    version_added = 'develop'
+    regex = re.compile(r"^\s+- requires(|_in|_any):")
diff --git a/setup.py b/setup.py
index d79f181..3a4616c 100644
--- a/setup.py
+++ b/setup.py
@@ -45,7 +45,7 @@ def long_description():
 setup(
     name=NAME,
     version=VERSION,
-    description=DESCRIPTION.split('\n')[0],
+    description=DESCRIPTION.split('\n')[0],  # pylint: disable=C0207
     long_description=long_description(),
     long_description_content_type='text/markdown',
     author=__author__,


Test patch:
diff --git a/tests/unit/TestTypoContentsRule.py b/tests/unit/TestTypoContentsRule.py
new file mode 100644
index 0000000..9fd62a9
--- /dev/null
+++ b/tests/unit/TestTypoContentsRule.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
+# Copyright (c) 2018 Ansible by Red Hat
+# Modified work Copyright (c) 2020 Warpnet B.V.
+# Copyright (c) 2021 Yury Bushmelev
+
+import unittest
+
+from saltlint.linter.collection import RulesCollection
+from saltlint.rules.TypoContentsRule import TypoContentsRule
+from tests import RunFromText
+
+
+GOOD_CONTENTS_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/testfile
+    - user: root
+    - group: root
+    - mode: '0700'
+    - contents: test
+    - contents_pillar: foo
+    - contents_grains: bar
+    - contents_newline: True
+    - contents_delimiter: '!'
+'''
+
+BAD_CONTENTS_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/badfile
+    - user: root
+    - group: root
+    - mode: 0700
+    - content: test
+    - content_pillar: foo
+    - content_grains: bar
+    - content_newline: True
+    - content_delimiter: '!'
+'''
+
+class TestTypoContentsRule(unittest.TestCase):
+    collection = RulesCollection()
+
+    def setUp(self):
+        self.collection.register(TypoContentsRule())
+        self.runner = RunFromText(self.collection)
+
+    def test_statement_positive(self):
+        results = self.runner.run_state(GOOD_CONTENTS_LINE)
+        self.assertEqual(0, len(results))
+
+    def test_statement_negative(self):
+        results = self.runner.run_state(BAD_CONTENTS_LINE)
+        self.assertEqual(5, len(results))
diff --git a/tests/unit/TestTypoOnchangesRule.py b/tests/unit/TestTypoOnchangesRule.py
new file mode 100644
index 0000000..39924f1
--- /dev/null
+++ b/tests/unit/TestTypoOnchangesRule.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
+# Copyright (c) 2018 Ansible by Red Hat
+# Modified work Copyright (c) 2020 Warpnet B.V.
+# Copyright (c) 2021 Yury Bushmelev
+
+import unittest
+
+from saltlint.linter.collection import RulesCollection
+from saltlint.rules.TypoOnchangesRule import TypoOnchangesRule
+from tests import RunFromText
+
+
+GOOD_ONCHANGES_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/testfile
+    - user: root
+    - group: root
+    - mode: '0700'
+    - onchanges:
+        - otherfile
+    - onchanges_in:
+        - anotherfile
+    - onchanges_any:
+        - yetanotherfile
+        - onemorefile
+'''
+
+BAD_ONCHANGES_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/badfile
+    - user: root
+    - group: root
+    - mode: 0700
+    - onchange:
+        - otherfile
+    - onchange_in:
+        - anotherfile
+    - onchange_any:
+        - yetanotherfile
+        - onemorefile
+'''
+
+class TestTypoOnchangesRule(unittest.TestCase):
+    collection = RulesCollection()
+
+    def setUp(self):
+        self.collection.register(TypoOnchangesRule())
+        self.runner = RunFromText(self.collection)
+
+    def test_statement_positive(self):
+        results = self.runner.run_state(GOOD_ONCHANGES_LINE)
+        self.assertEqual(0, len(results))
+
+    def test_statement_negative(self):
+        results = self.runner.run_state(BAD_ONCHANGES_LINE)
+        self.assertEqual(3, len(results))
diff --git a/tests/unit/TestTypoRequireRule.py b/tests/unit/TestTypoRequireRule.py
new file mode 100644
index 0000000..67d3dc8
--- /dev/null
+++ b/tests/unit/TestTypoRequireRule.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
+# Copyright (c) 2018 Ansible by Red Hat
+# Modified work Copyright (c) 2020 Warpnet B.V.
+# Copyright (c) 2021 Yury Bushmelev
+
+import unittest
+
+from saltlint.linter.collection import RulesCollection
+from saltlint.rules.TypoRequireRule import TypoRequireRule
+from tests import RunFromText
+
+
+GOOD_REQUIRE_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/testfile
+    - user: root
+    - group: root
+    - mode: '0700'
+    - require:
+        - otherfile
+    - require_in:
+        - anotherfile
+    - require_any:
+        - yetanotherfile
+        - onemorefile
+'''
+
+BAD_REQUIRE_LINE = '''
+testfile:
+  file.managed:
+    - name: /tmp/badfile
+    - user: root
+    - group: root
+    - mode: 0700
+    - requires:
+        - otherfile
+    - requires_in:
+        - anotherfile
+    - requires_any:
+        - yetanotherfile
+        - onemorefile
+'''
+
+class TestTypoRequireRule(unittest.TestCase):
+    collection = RulesCollection()
+
+    def setUp(self):
+        self.collection.register(TypoRequireRule())
+        self.runner = RunFromText(self.collection)
+
+    def test_statement_positive(self):
+        results = self.runner.run_state(GOOD_REQUIRE_LINE)
+        self.assertEqual(0, len(results))
+
+    def test_statement_negative(self):
+        results = self.runner.run_state(BAD_REQUIRE_LINE)
+        self.assertEqual(3, len(results))