Repository: warpnet/salt-lint
Pull request #203 (2020-11-28 11:28:45)

Improve readability of code
Improved readability by:
- Splitting the `linter.py` and formatters in separate files.
- Renamed `matches` to `problems`.
- Updated some variable names.

Also updated the copyright notices to 2020.


Patch:
diff --git a/LICENSE b/LICENSE
index c3f0a06..d3a293b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,7 +2,7 @@ MIT License
 
 Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 Copyright (c) 2018 Ansible by Red Hat
-Modified work Copyright (c) 2019 Warpnet B.V.
+Modified work Copyright (c) 2020 Warpnet B.V.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/saltlint/__init__.py b/saltlint/__init__.py
index ec9110f..0c41c14 100644
--- a/saltlint/__init__.py
+++ b/saltlint/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2019 Warpnet B.V.
+# Copyright (c) 2020 Warpnet B.V.
 
 """A command-line utility that checks for best practices in SaltStack.
 """
diff --git a/saltlint/__main__.py b/saltlint/__main__.py
index 4484a68..716bea2 100644
--- a/saltlint/__main__.py
+++ b/saltlint/__main__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import sys
 import errno
diff --git a/saltlint/cli.py b/saltlint/cli.py
index 00b3173..e198ea7 100644
--- a/saltlint/cli.py
+++ b/saltlint/cli.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 from __future__ import print_function
 
@@ -12,8 +12,9 @@ import codecs
 
 from saltlint import NAME, VERSION, DESCRIPTION
 from saltlint import formatters
-from saltlint.config import SaltLintConfig, SaltLintConfigError, default_rulesdir
-from saltlint.linter import RulesCollection, Runner
+from saltlint.config import Configuration, SaltLintConfigError, default_rulesdir
+from saltlint.linter.collection import RulesCollection
+from saltlint.linter.runner import Runner
 
 
 def run(args=None):
@@ -41,7 +42,7 @@ def run(args=None):
     # Read, parse and validate the configuration
     options_dict = vars(options)
     try:
-        config = SaltLintConfig(options_dict)
+        config = Configuration(options_dict)
     except SaltLintConfigError as exc:
         print(exc)
         return 2
@@ -52,25 +53,25 @@ def run(args=None):
         return 1
 
     # Collect the rules from the configuration
-    rules = RulesCollection(config)
+    collection = RulesCollection(config)
     for rulesdir in config.rulesdirs:
-        rules.extend(RulesCollection.create_from_directory(rulesdir, config))
+        collection.extend(RulesCollection.create_from_directory(rulesdir, config))
 
     # Show the rules listing
     if options.listrules:
-        print(rules)
+        print(collection)
         return 0
 
     # Show the tags listing
     if options.listtags:
-        print(rules.listtags())
+        print(collection.listtags())
         return 0
 
     formatter = initialize_formatter(config)
 
     problems = []
     for file_name in file_names:
-        runner = Runner(rules, file_name, config, checked_files)
+        runner = Runner(collection, file_name, config, checked_files)
         problems.extend(runner.run())
 
     # Delete stdin temporary file
diff --git a/saltlint/config.py b/saltlint/config.py
index 0c61344..9d23c8b 100644
--- a/saltlint/config.py
+++ b/saltlint/config.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2019 Warpnet B.V.
+# Copyright (c) 2020 Warpnet B.V.
 
 import os
 import sys
@@ -18,7 +18,7 @@ class SaltLintConfigError(Exception):
     pass
 
 
-class SaltLintConfig(object):
+class Configuration(object):
 
     def __init__(self, options={}):
         self._options = options
diff --git a/saltlint/formatters/__init__.py b/saltlint/formatters/__init__.py
index 4a5f4ee..1cc6888 100644
--- a/saltlint/formatters/__init__.py
+++ b/saltlint/formatters/__init__.py
@@ -1,108 +1,6 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
-# Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
-import json
-
-
-class BaseFormatter(object):
-
-    def __init__(self, colored=False):
-        self.colored = colored
-
-    def process(self, problems):
-        for problem in problems:
-            print(self.format(problem))
-
-    def format(self, problem):
-        raise NotImplementedError()
-
-
-class Formatter(BaseFormatter):
-
-    def format(self, problem):
-        formatstr = u"{0} {1}\n{2}:{3}\n{4}\n"
-
-        color = get_colors(self.colored)
-        return formatstr.format(
-            u'{0}[{1}]{2}'.format(color['RED'], problem.rule.id,
-                                  color['ENDC']),
-            u'{0}{1}{2}'.format(color['LIGHT_RED'], problem.message,
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['BLUE'], problem.filename,
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['CYAN'], str(problem.linenumber),
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['MAGENTA'], problem.line, color['ENDC'])
-        )
-
-
-class SeverityFormatter(BaseFormatter):
-
-    def format(self, problem):
-        formatstr = u"{0} {sev} {1}\n{2}:{3}\n{4}\n"
-
-        color = get_colors(self.colored)
-        return formatstr.format(
-            u'{0}[{1}]{2}'.format(color['RED'], problem.rule.id,
-                                  color['ENDC']),
-            u'{0}{1}{2}'.format(color['LIGHT_RED'], problem.message,
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['BLUE'], problem.filename,
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['CYAN'], str(problem.linenumber),
-                                color['ENDC']),
-            u'{0}{1}{2}'.format(color['MAGENTA'], problem.line, color['ENDC']),
-            sev=u'{0}[{1}]{2}'.format(color['RED'], problem.rule.severity,
-                                      color['ENDC'])
-        )
-
-
-class JsonFormatter(BaseFormatter):
-    def process(self, problems, *args, **kwargs):
-        items = []
-        for problem in problems:
-            items.append(self.format(problem))
-        print(json.dumps(items))
-
-    def format(self, problem):
-        return {
-            'id': problem.rule.id,
-            'message': problem.message,
-            'filename': problem.filename,
-            'linenumber': problem.linenumber,
-            'line': problem.line,
-            'severity': problem.rule.severity,
-        }
-
-
-def get_colors(use=True):
-    """
-    Return the colors as a dict, pass False to return the colors as empty
-    strings.
-    """
-    colors = {
-        "BLACK": "\033[0;30m",
-        "DARK_GRAY": "\033[1;30m",
-        "RED": "\033[0;31m",
-        "LIGHT_RED": "\033[1;31m",
-        "GREEN": "\033[0;32m",
-        "LIGHT_GREEN": "\033[1;32m",
-        "BLUE": "\033[0;34m",
-        "LIGHT_BLUE": "\033[1;34m",
-        "MAGENTA": "\033[0;35m",
-        "LIGHT_MAGENTA": "\033[1;35m",
-        "CYAN": "\033[0;36m",
-        "LIGHT_CYAN": "\033[1;36m",
-        "LIGHT_GRAY": "\033[0;37m",
-        "WHITE": "\033[1;37m",
-        "DEFAULT_COLOR": "\033[00m",
-        "ENDC": "\033[0m",
-    }
-
-    if not use:
-        for color in colors:
-            colors[color] = ''
-
-    return colors
+from saltlint.formatters.default import Formatter
+from saltlint.formatters.severity import SeverityFormatter
+from saltlint.formatters.json import JsonFormatter
diff --git a/saltlint/formatters/base.py b/saltlint/formatters/base.py
new file mode 100644
index 0000000..e01ed4b
--- /dev/null
+++ b/saltlint/formatters/base.py
@@ -0,0 +1,47 @@
+# -*- 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.
+
+
+class BaseFormatter(object):
+
+    def __init__(self, colored=False):
+        self.colored = colored
+
+    def process(self, problems):
+        for problem in problems:
+            print(self.format(problem))
+
+    def format(self, problem):
+        raise NotImplementedError()
+
+    def get_colors(self, use=True):
+        """
+        Return the colors as a dict, pass False to return the colors as empty
+        strings.
+        """
+        colors = {
+            "BLACK": "\033[0;30m",
+            "DARK_GRAY": "\033[1;30m",
+            "RED": "\033[0;31m",
+            "LIGHT_RED": "\033[1;31m",
+            "GREEN": "\033[0;32m",
+            "LIGHT_GREEN": "\033[1;32m",
+            "BLUE": "\033[0;34m",
+            "LIGHT_BLUE": "\033[1;34m",
+            "MAGENTA": "\033[0;35m",
+            "LIGHT_MAGENTA": "\033[1;35m",
+            "CYAN": "\033[0;36m",
+            "LIGHT_CYAN": "\033[1;36m",
+            "LIGHT_GRAY": "\033[0;37m",
+            "WHITE": "\033[1;37m",
+            "DEFAULT_COLOR": "\033[00m",
+            "ENDC": "\033[0m",
+        }
+
+        if not use:
+            for color in colors:
+                colors[color] = ''
+
+        return colors
diff --git a/saltlint/formatters/default.py b/saltlint/formatters/default.py
new file mode 100644
index 0000000..4b5ad50
--- /dev/null
+++ b/saltlint/formatters/default.py
@@ -0,0 +1,26 @@
+# -*- 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.
+
+
+from saltlint.formatters.base import BaseFormatter
+
+
+class Formatter(BaseFormatter):
+
+    def format(self, problem):
+        formatstr = u"{0} {1}\n{2}:{3}\n{4}\n"
+
+        color = self.get_colors(self.colored)
+        return formatstr.format(
+            u'{0}[{1}]{2}'.format(color['RED'], problem.rule.id,
+                                  color['ENDC']),
+            u'{0}{1}{2}'.format(color['LIGHT_RED'], problem.message,
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['BLUE'], problem.filename,
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['CYAN'], str(problem.linenumber),
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['MAGENTA'], problem.line, color['ENDC'])
+        )
diff --git a/saltlint/formatters/json.py b/saltlint/formatters/json.py
new file mode 100644
index 0000000..c7134fb
--- /dev/null
+++ b/saltlint/formatters/json.py
@@ -0,0 +1,26 @@
+# -*- 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.
+
+import json
+
+from saltlint.formatters.base import BaseFormatter
+
+
+class JsonFormatter(BaseFormatter):
+    def process(self, problems, *args, **kwargs):
+        items = []
+        for problem in problems:
+            items.append(self.format(problem))
+        print(json.dumps(items))
+
+    def format(self, problem):
+        return {
+            'id': problem.rule.id,
+            'message': problem.message,
+            'filename': problem.filename,
+            'linenumber': problem.linenumber,
+            'line': problem.line,
+            'severity': problem.rule.severity,
+        }
diff --git a/saltlint/formatters/severity.py b/saltlint/formatters/severity.py
new file mode 100644
index 0000000..904521a
--- /dev/null
+++ b/saltlint/formatters/severity.py
@@ -0,0 +1,27 @@
+# -*- 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.
+
+from saltlint.formatters.base import BaseFormatter
+
+
+class SeverityFormatter(BaseFormatter):
+
+    def format(self, problem):
+        formatstr = u"{0} {sev} {1}\n{2}:{3}\n{4}\n"
+
+        color = self.get_colors(self.colored)
+        return formatstr.format(
+            u'{0}[{1}]{2}'.format(color['RED'], problem.rule.id,
+                                  color['ENDC']),
+            u'{0}{1}{2}'.format(color['LIGHT_RED'], problem.message,
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['BLUE'], problem.filename,
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['CYAN'], str(problem.linenumber),
+                                color['ENDC']),
+            u'{0}{1}{2}'.format(color['MAGENTA'], problem.line, color['ENDC']),
+            sev=u'{0}[{1}]{2}'.format(color['RED'], problem.rule.severity,
+                                      color['ENDC'])
+        )
diff --git a/saltlint/linter.py b/saltlint/linter.py
deleted file mode 100644
index 83e12be..0000000
--- a/saltlint/linter.py
+++ /dev/null
@@ -1,221 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
-# Modified work Copyright (c) 2019 Warpnet B.V.
-
-from __future__ import print_function
-
-from collections import defaultdict
-import os
-import re
-import sys
-import codecs
-import six
-
-import saltlint.utils
-from saltlint.config import SaltLintConfig
-
-
-class SaltLintRule(object):
-
-    def __init__(self, config=None):
-        self.config = config
-
-    def __repr__(self):
-        return self.id + ": " + self.shortdesc
-
-    def verbose(self):
-        return self.id + ": " + self.shortdesc + "\n " + self.description
-
-    match = None
-    matchtext = None
-
-    @staticmethod
-    def unjinja(text):
-        return re.sub(r"{{[^}]*}}", "JINJA_VAR", text)
-
-    def matchlines(self, file, text):
-        matches = []
-        if not self.match:
-            return matches
-
-        # arrays are 0-based, line numbers are 1-based
-        # so use prev_line_no as the counter
-        for (prev_line_no, line) in enumerate(text.split("\n")):
-            if line.lstrip().startswith('#'):
-                continue
-
-            rule_id_list = saltlint.utils.get_rule_skips_from_line(line)
-            if self.id in rule_id_list:
-                continue
-
-            result = self.match(file, line)
-            if not result:
-                continue
-            message = None
-            if isinstance(result, six.string_types):
-                message = result
-            matches.append(Match(prev_line_no+1, line,
-                                 file['path'], self, message))
-
-        return matches
-
-    def matchfulltext(self, file, text):
-        matches = []
-        if not self.matchtext:
-            return matches
-
-        results = self.matchtext(file, text)
-
-        for line, section, message in results:
-            matches.append(Match(line, section, file['path'], self, message))
-
-        return matches
-
-
-class RulesCollection(object):
-
-    def __init__(self, config=SaltLintConfig()):
-        self.rules = []
-        self.config = config
-
-    def register(self, obj):
-        if not any(rule.id == obj.id for rule in self.rules):
-            self.rules.append(obj)
-
-    def __iter__(self):
-        return iter(self.rules)
-
-    def __len__(self):
-        return len(self.rules)
-
-    def extend(self, more):
-        self.rules.extend(more)
-
-    def run(self, statefile, tags=set(), skip_list=frozenset()):
-        text = ""
-        matches = []
-
-        try:
-            with codecs.open(statefile['path'], mode='rb', encoding='utf-8') as f:
-                text = f.read()
-        except IOError as e:
-            print("WARNING: Couldn't open %s - %s" %
-                  (statefile['path'], e.strerror),
-                  file=sys.stderr)
-            return matches
-
-        for rule in self.rules:
-            skip = False
-            if not tags or not set(rule.tags).union([rule.id]).isdisjoint(tags):
-                rule_definition = set(rule.tags)
-                rule_definition.add(rule.id)
-
-                # Check if the file is in the rule specific ignore list.
-                for definition in rule_definition:
-                    if self.config.is_file_ignored(statefile['path'], definition):
-                        skip = True
-                        break
-
-                if not skip and set(rule_definition).isdisjoint(skip_list):
-                    matches.extend(rule.matchlines(statefile, text))
-                    matches.extend(rule.matchfulltext(statefile, text))
-
-        return matches
-
-    def __repr__(self):
-        return "\n".join([rule.verbose()
-                          for rule in sorted(self.rules, key=lambda x: x.id)])
-
-    def listtags(self):
-        tags = defaultdict(list)
-        for rule in self.rules:
-            for tag in rule.tags:
-                tags[tag].append("[{0}]".format(rule.id))
-        results = []
-        for tag in sorted(tags):
-            results.append("{0} {1}".format(tag, tags[tag]))
-        return "\n".join(results)
-
-    @classmethod
-    def create_from_directory(cls, rulesdir, config):
-        result = cls(config)
-        result.rules = saltlint.utils.load_plugins(os.path.expanduser(rulesdir), config)
-        return result
-
-
-class Match(object):
-
-    def __init__(self, linenumber, line, filename, rule, message=None):
-        self.linenumber = linenumber
-        self.line = line
-        self.filename = filename
-        self.rule = rule
-        self.message = message or rule.shortdesc
-
-    def __repr__(self):
-        formatstr = u"[{0}] ({1}) matched {2}:{3} {4}"
-        return formatstr.format(self.rule.id, self.message,
-                                self.filename, self.linenumber, self.line)
-
-
-class Runner(object):
-
-    def __init__(self, rules, state, config, checked_files=None):
-        self.rules = rules
-
-        self.states = set()
-        # Assume state is directory
-        if os.path.isdir(state):
-            self.states.add((os.path.join(state, 'init.sls'), 'state'))
-        else:
-            self.states.add((state, 'state'))
-
-        # Get configuration options
-        self.config = config
-        self.tags = config.tags
-        self.skip_list = config.skip_list
-        self.verbosity = config.verbosity
-        self._update_exclude_paths(config.exclude_paths)
-
-        # Set the checked files
-        if checked_files is None:
-            checked_files = set()
-        self.checked_files = checked_files
-
-    def _update_exclude_paths(self, exclude_paths):
-        if exclude_paths:
-            # These will be (potentially) relative paths
-            paths = [s.strip() for s in exclude_paths]
-            self.exclude_paths = paths + [os.path.abspath(p) for p in paths]
-        else:
-            self.exclude_paths = []
-
-    def is_excluded(self, file_path):
-        # Any will short-circuit as soon as something returns True, but will
-        # be poor performance for the case where the path under question is
-        # not excluded.
-        return any(file_path.startswith(path) for path in self.exclude_paths)
-
-    def run(self):
-        files = []
-        for state in self.states:
-            if self.is_excluded(state[0]):
-                continue
-            files.append({'path': state[0], 'type': state[1]})
-
-        matches = []
-
-        # remove duplicates from files list
-        files = [value for n, value in enumerate(files) if value not in files[:n]]
-
-        # remove files that have already been checked
-        files = [x for x in files if x['path'] not in self.checked_files]
-        for file in files:
-            if self.verbosity > 0:
-                print("Examining %s of type %s" % (file['path'], file['type']))
-            matches.extend(self.rules.run(file, tags=set(self.tags),
-                                          skip_list=self.skip_list))
-        # update list of checked files
-        self.checked_files.update([x['path'] for x in files])
-
-        return matches
diff --git a/saltlint/linter/__init__.py b/saltlint/linter/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saltlint/linter/collection.py b/saltlint/linter/collection.py
new file mode 100644
index 0000000..bf09619
--- /dev/null
+++ b/saltlint/linter/collection.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+# Modified work Copyright (c) 2020 Warpnet B.V.
+
+from __future__ import print_function
+
+from collections import defaultdict
+import os
+import codecs
+import sys
+
+from saltlint.config import Configuration
+from saltlint.utils import load_plugins
+
+
+class RulesCollection(object):
+
+    def __init__(self, config=Configuration()):
+        self.rules = []
+        self.config = config
+
+    def register(self, obj):
+        if not any(rule.id == obj.id for rule in self.rules):
+            self.rules.append(obj)
+
+    def __iter__(self):
+        return iter(self.rules)
+
+    def __len__(self):
+        return len(self.rules)
+
+    def extend(self, more):
+        self.rules.extend(more)
+
+    def run(self, statefile, tags=set(), skip_list=frozenset()):
+        text = ""
+        matches = []
+
+        try:
+            with codecs.open(statefile['path'], mode='rb', encoding='utf-8') as f:
+                text = f.read()
+        except IOError as e:
+            print("WARNING: Couldn't open %s - %s" %
+                  (statefile['path'], e.strerror),
+                  file=sys.stderr)
+            return matches
+
+        for rule in self.rules:
+            skip = False
+            if not tags or not set(rule.tags).union([rule.id]).isdisjoint(tags):
+                rule_definition = set(rule.tags)
+                rule_definition.add(rule.id)
+
+                # Check if the file is in the rule specific ignore list.
+                for definition in rule_definition:
+                    if self.config.is_file_ignored(statefile['path'], definition):
+                        skip = True
+                        break
+
+                if not skip and set(rule_definition).isdisjoint(skip_list):
+                    matches.extend(rule.matchlines(statefile, text))
+                    matches.extend(rule.matchfulltext(statefile, text))
+
+        return matches
+
+    def __repr__(self):
+        return "\n".join([rule.verbose()
+                          for rule in sorted(self.rules, key=lambda x: x.id)])
+
+    def listtags(self):
+        tags = defaultdict(list)
+        for rule in self.rules:
+            for tag in rule.tags:
+                tags[tag].append("[{0}]".format(rule.id))
+        results = []
+        for tag in sorted(tags):
+            results.append("{0} {1}".format(tag, tags[tag]))
+        return "\n".join(results)
+
+    @classmethod
+    def create_from_directory(cls, rulesdir, config):
+        result = cls(config)
+        result.rules = load_plugins(os.path.expanduser(rulesdir), config)
+        return result
diff --git a/saltlint/linter/match.py b/saltlint/linter/match.py
new file mode 100644
index 0000000..f3aa709
--- /dev/null
+++ b/saltlint/linter/match.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+# Modified work Copyright (c) 2020 Warpnet B.V.
+
+
+class Match(object):
+
+    def __init__(self, linenumber, line, filename, rule, message=None):
+        self.linenumber = linenumber
+        self.line = line
+        self.filename = filename
+        self.rule = rule
+        self.message = message or rule.shortdesc
+
+    def __repr__(self):
+        formatstr = u"[{0}] ({1}) matched {2}:{3} {4}"
+        return formatstr.format(self.rule.id, self.message,
+                                self.filename, self.linenumber, self.line)
diff --git a/saltlint/linter/rule.py b/saltlint/linter/rule.py
new file mode 100644
index 0000000..bf95848
--- /dev/null
+++ b/saltlint/linter/rule.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+# Modified work Copyright (c) 2020 Warpnet B.V.
+
+import re
+import six
+
+from saltlint.utils import get_rule_skips_from_line
+from saltlint.linter.match import Match
+
+
+class Rule(object):
+
+    def __init__(self, config=None):
+        self.config = config
+
+    def __repr__(self):
+        return self.id + ": " + self.shortdesc
+
+    def verbose(self):
+        return self.id + ": " + self.shortdesc + "\n " + self.description
+
+    match = None
+    matchtext = None
+
+    @staticmethod
+    def unjinja(text):
+        return re.sub(r"{{[^}]*}}", "JINJA_VAR", text)
+
+    def matchlines(self, file, text):
+        matches = []
+        if not self.match:
+            return matches
+
+        # arrays are 0-based, line numbers are 1-based
+        # so use prev_line_no as the counter
+        for (prev_line_no, line) in enumerate(text.split("\n")):
+            if line.lstrip().startswith('#'):
+                continue
+
+            rule_id_list = get_rule_skips_from_line(line)
+            if self.id in rule_id_list:
+                continue
+
+            result = self.match(file, line)
+            if not result:
+                continue
+            message = None
+            if isinstance(result, six.string_types):
+                message = result
+            matches.append(Match(prev_line_no+1, line,
+                                 file['path'], self, message))
+
+        return matches
+
+    def matchfulltext(self, file, text):
+        matches = []
+        if not self.matchtext:
+            return matches
+
+        results = self.matchtext(file, text)
+
+        for line, section, message in results:
+            matches.append(Match(line, section, file['path'], self, message))
+
+        return matches
diff --git a/saltlint/linter/runner.py b/saltlint/linter/runner.py
new file mode 100644
index 0000000..3add57d
--- /dev/null
+++ b/saltlint/linter/runner.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+# Modified work Copyright (c) 2020 Warpnet B.V.
+
+from __future__ import print_function
+
+import os
+
+
+class Runner(object):
+
+    def __init__(self, collection, file_name, config, checked_files=None):
+        self.collection = collection
+
+        self.states = set()
+        # Assume the provided file name is a directory
+        if os.path.isdir(file_name):
+            self.states.add((os.path.join(file_name, 'init.sls'), 'state'))
+        else:
+            self.states.add((file_name, 'state'))
+
+        # Get configuration options
+        self.config = config
+        self.tags = config.tags
+        self.skip_list = config.skip_list
+        self.verbosity = config.verbosity
+        self._update_exclude_paths(config.exclude_paths)
+
+        # Set the checked files
+        if checked_files is None:
+            checked_files = set()
+        self.checked_files = checked_files
+
+    def _update_exclude_paths(self, exclude_paths):
+        if exclude_paths:
+            # These will be (potentially) relative paths
+            paths = [path.strip() for path in exclude_paths]
+            self.exclude_paths = paths + [os.path.abspath(path) for path in paths]
+        self.exclude_paths = []
+
+    def is_excluded(self, file_path):
+        # Any will short-circuit as soon as something returns True, but will
+        # be poor performance for the case where the path under question is
+        # not excluded.
+        return any(file_path.startswith(path) for path in self.exclude_paths)
+
+    def is_already_checked(self, file_path):
+        return file_path in self.checked_files
+
+    def run(self):
+        files = []
+        for index, state in enumerate(self.states):
+            file_path = state[0]
+            file_type = state[1]
+            file_dict = {'path': file_path, 'type': file_type}
+            # Skip excluded files
+            if self.is_excluded(file_path):
+                continue
+            # Skip already checked files
+            if self.is_already_checked(file_path):
+                continue
+            # Skip duplicates
+            if file_dict in files[:index]:
+                continue
+            files.append(file_dict)
+
+        problems = []
+        for file in files:
+            if self.verbosity > 0:
+                print("Examining %s of type %s" % (file['path'], file['type']))
+            problems.extend(self.collection.run(file, tags=set(self.tags),
+                                                skip_list=self.skip_list))
+        # Update list of checked files
+        self.checked_files.update([file_dict['path'] for file_dict in files])
+
+        return problems
diff --git a/saltlint/rules/FileExtensionRule.py b/saltlint/rules/FileExtensionRule.py
index e5c1222..c8157e5 100644
--- a/saltlint/rules/FileExtensionRule.py
+++ b/saltlint/rules/FileExtensionRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Tsukinowa Inc. <info@tsukinowa.jp>
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import os
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class FileExtensionRule(SaltLintRule):
+class FileExtensionRule(Rule):
     id = '205'
     shortdesc = 'Use ".sls" as a Salt State file extension'
     description = 'Salt State files should have the ".sls" extension'
diff --git a/saltlint/rules/FileModeLeadingZeroRule.py b/saltlint/rules/FileModeLeadingZeroRule.py
index c8fd53c..226bea3 100644
--- a/saltlint/rules/FileModeLeadingZeroRule.py
+++ b/saltlint/rules/FileModeLeadingZeroRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class FileModeLeadingZeroRule(SaltLintRule):
+class FileModeLeadingZeroRule(Rule):
     id = '208'
     shortdesc = 'File modes should always contain a leading zero'
     description = 'File modes should always contain a leading zero'
diff --git a/saltlint/rules/FileModeQuotationRule.py b/saltlint/rules/FileModeQuotationRule.py
index c0329e6..e449c0c 100644
--- a/saltlint/rules/FileModeQuotationRule.py
+++ b/saltlint/rules/FileModeQuotationRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class FileModeQuotationRule(SaltLintRule):
+class FileModeQuotationRule(Rule):
     id = '207'
     shortdesc = 'File modes should always be encapsulated in quotation marks'
     description = 'File modes should always be encapsulated in quotation marks'
diff --git a/saltlint/rules/JinjaCommentHasSpacesRule.py b/saltlint/rules/JinjaCommentHasSpacesRule.py
index 1ff70ce..6caa1b6 100644
--- a/saltlint/rules/JinjaCommentHasSpacesRule.py
+++ b/saltlint/rules/JinjaCommentHasSpacesRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class JinjaCommentHasSpacesRule(SaltLintRule):
+class JinjaCommentHasSpacesRule(Rule):
     id = '209'
     shortdesc = 'Jinja comment should have spaces before and after: {# comment #}'
     description = 'Jinja comment should have spaces before and after: ``{# comment #}``'
diff --git a/saltlint/rules/JinjaPillarGrainsGetFormatRule.py b/saltlint/rules/JinjaPillarGrainsGetFormatRule.py
index b65d7d4..efd8181 100644
--- a/saltlint/rules/JinjaPillarGrainsGetFormatRule.py
+++ b/saltlint/rules/JinjaPillarGrainsGetFormatRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class JinjaPillarGrainsGetFormatRule(SaltLintRule):
+class JinjaPillarGrainsGetFormatRule(Rule):
     id = '211'
     shortdesc = 'pillar.get or grains.get should be formatted differently'
     description = "pillar.get and grains.get should always be formatted " \
diff --git a/saltlint/rules/JinjaStatementHasSpacesRule.py b/saltlint/rules/JinjaStatementHasSpacesRule.py
index 771d471..6bcf290 100644
--- a/saltlint/rules/JinjaStatementHasSpacesRule.py
+++ b/saltlint/rules/JinjaStatementHasSpacesRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class JinjaStatementHasSpacesRule(SaltLintRule):
+class JinjaStatementHasSpacesRule(Rule):
     id = '202'
     shortdesc = 'Jinja statement should have spaces before and after: {% statement %}'
     description = 'Jinja statement should have spaces before and after: ``{% statement %}``'
diff --git a/saltlint/rules/JinjaVariableHasSpacesRule.py b/saltlint/rules/JinjaVariableHasSpacesRule.py
index 0941753..f864fce 100644
--- a/saltlint/rules/JinjaVariableHasSpacesRule.py
+++ b/saltlint/rules/JinjaVariableHasSpacesRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class JinjaVariableHasSpacesRule(SaltLintRule):
+class JinjaVariableHasSpacesRule(Rule):
     id = '206'
     shortdesc = 'Jinja variables should have spaces before and after: {{ var_name }}'
     description = 'Jinja variables should have spaces before and after: ``{{ var_name }}``'
diff --git a/saltlint/rules/LineTooLongRule.py b/saltlint/rules/LineTooLongRule.py
index d3b5537..aa8cfd2 100644
--- a/saltlint/rules/LineTooLongRule.py
+++ b/saltlint/rules/LineTooLongRule.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class LineTooLongRule(SaltLintRule):
+class LineTooLongRule(Rule):
     id = '204'
     shortdesc = 'Lines should be no longer that 160 chars'
     description = (
diff --git a/saltlint/rules/NoIrregularSpacesRule.py b/saltlint/rules/NoIrregularSpacesRule.py
index 8b77506..53a371f 100644
--- a/saltlint/rules/NoIrregularSpacesRule.py
+++ b/saltlint/rules/NoIrregularSpacesRule.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
 
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class NoIrregularSpacesRule(SaltLintRule):
+class NoIrregularSpacesRule(Rule):
     id = '212'
     shortdesc = 'Most files should not contain irregular spaces'
     description = 'Irregular spaces can cause unexpected display issues, use spaces'
diff --git a/saltlint/rules/NoTabsRule.py b/saltlint/rules/NoTabsRule.py
index a143f52..57bfa90 100644
--- a/saltlint/rules/NoTabsRule.py
+++ b/saltlint/rules/NoTabsRule.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class NoTabsRule(SaltLintRule):
+class NoTabsRule(Rule):
     id = '203'
     shortdesc = 'Most files should not contain tabs'
     description = 'Tabs can cause unexpected display issues, use spaces'
diff --git a/saltlint/rules/TrailingWhitespaceRule.py b/saltlint/rules/TrailingWhitespaceRule.py
index 199e3d5..6ec294d 100644
--- a/saltlint/rules/TrailingWhitespaceRule.py
+++ b/saltlint/rules/TrailingWhitespaceRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class TrailingWhitespaceRule(SaltLintRule):
+class TrailingWhitespaceRule(Rule):
     id = '201'
     shortdesc = 'Trailing whitespace'
     description = 'There should not be any trailing whitespace'
diff --git a/saltlint/rules/YamlHasOctalValueRule.py b/saltlint/rules/YamlHasOctalValueRule.py
index b40d96b..7633f9f 100644
--- a/saltlint/rules/YamlHasOctalValueRule.py
+++ b/saltlint/rules/YamlHasOctalValueRule.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2016 Will Thames and contributors
 # Copyright (c) 2018 Ansible Project
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import re
-from saltlint.linter import SaltLintRule
+from saltlint.linter.rule import Rule
 
 
-class YamlHasOctalValueRule(SaltLintRule):
+class YamlHasOctalValueRule(Rule):
     id = '210'
     shortdesc = 'Numbers that start with `0` should always be encapsulated in quotation marks'
     description = 'Numbers that start with `0` should always be encapsulated in quotation marks'
diff --git a/saltlint/utils.py b/saltlint/utils.py
index 6d01e0e..a0334b2 100644
--- a/saltlint/utils.py
+++ b/saltlint/utils.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import glob
 import imp


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 316e73a..6c72cf6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,11 +1,11 @@
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import tempfile
 
-from saltlint.linter import Runner
-from saltlint.config import SaltLintConfig
+from saltlint.linter.runner import Runner
+from saltlint.config import Configuration
 
 
 class RunFromText(object):
@@ -15,7 +15,7 @@ class RunFromText(object):
         self.collection = collection
 
     def _call_runner(self, path):
-        runner = Runner(self.collection, path, SaltLintConfig())
+        runner = Runner(self.collection, path, Configuration())
         return runner.run()
 
     def run_state(self, state_text):
diff --git a/tests/unit/TestConfig.py b/tests/unit/TestConfig.py
index 4cde49f..cead988 100644
--- a/tests/unit/TestConfig.py
+++ b/tests/unit/TestConfig.py
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2019 Warpnet B.V.
+# Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 import tempfile
 
-from saltlint.config import SaltLintConfig
+from saltlint.config import Configuration
 
 
 SALT_LINT_CONFIG = '''
@@ -38,7 +38,7 @@ class TestConfig(unittest.TestCase):
 
         # Specify the temporary file as if it was passed as a command line
         # argument.
-        self.config = SaltLintConfig(dict(c=self.fp.name))
+        self.config = Configuration(dict(c=self.fp.name))
 
     def tearDown(self):
         # Close the temporary configuration file.
diff --git a/tests/unit/TestFileExtension.py b/tests/unit/TestFileExtension.py
index 667054c..2e985c0 100644
--- a/tests/unit/TestFileExtension.py
+++ b/tests/unit/TestFileExtension.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2019 Warpnet B.V.
+# Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.config import SaltLintConfig
-from saltlint.linter import Runner, RulesCollection
+from saltlint.config import Configuration
+from saltlint.linter.runner import Runner
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.FileExtensionRule import FileExtensionRule
 
 
@@ -16,11 +17,11 @@ class TestLineTooLongRule(unittest.TestCase):
 
     def test_file_positive(self):
         path = 'tests/test-extension-success.sls'
-        runner = Runner(self.collection, path, SaltLintConfig())
+        runner = Runner(self.collection, path, Configuration())
         self.assertEqual([], runner.run())
 
     def test_file_negative(self):
         path = 'tests/test-extension-failure'
-        runner = Runner(self.collection, path, SaltLintConfig())
+        runner = Runner(self.collection, path, Configuration())
         errors = runner.run()
         self.assertEqual(1, len(errors))
diff --git a/tests/unit/TestFileModeLeadingZeroRule.py b/tests/unit/TestFileModeLeadingZeroRule.py
index 30156bf..7f842c8 100644
--- a/tests/unit/TestFileModeLeadingZeroRule.py
+++ b/tests/unit/TestFileModeLeadingZeroRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.FileModeLeadingZeroRule import FileModeLeadingZeroRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestFileModeQuotationRule.py b/tests/unit/TestFileModeQuotationRule.py
index 13fcff1..b2d2088 100644
--- a/tests/unit/TestFileModeQuotationRule.py
+++ b/tests/unit/TestFileModeQuotationRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.FileModeQuotationRule import FileModeQuotationRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestJinjaCommentHasSpaces.py b/tests/unit/TestJinjaCommentHasSpaces.py
index d8dae99..60796fe 100644
--- a/tests/unit/TestJinjaCommentHasSpaces.py
+++ b/tests/unit/TestJinjaCommentHasSpaces.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.JinjaCommentHasSpacesRule import JinjaCommentHasSpacesRule
 from tests import RunFromText
 
@@ -18,7 +18,7 @@ BAD_COMMENT_LINE = '''
 {#-set example='bad'+#}
 '''
 
-class TestLineTooLongRule(unittest.TestCase):
+class TestJinjaCommentHasSpaces(unittest.TestCase):
     collection = RulesCollection()
 
     def setUp(self):
diff --git a/tests/unit/TestJinjaPillarGrainsGetFormatRule.py b/tests/unit/TestJinjaPillarGrainsGetFormatRule.py
index 6293f3b..ec370a2 100644
--- a/tests/unit/TestJinjaPillarGrainsGetFormatRule.py
+++ b/tests/unit/TestJinjaPillarGrainsGetFormatRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.JinjaPillarGrainsGetFormatRule import JinjaPillarGrainsGetFormatRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestJinjaStatementHasSpaces.py b/tests/unit/TestJinjaStatementHasSpaces.py
index 9e965af..947a876 100644
--- a/tests/unit/TestJinjaStatementHasSpaces.py
+++ b/tests/unit/TestJinjaStatementHasSpaces.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.JinjaStatementHasSpacesRule import JinjaStatementHasSpacesRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestJinjaVariableHasSpaces.py b/tests/unit/TestJinjaVariableHasSpaces.py
index 1fd0448..301cb8d 100644
--- a/tests/unit/TestJinjaVariableHasSpaces.py
+++ b/tests/unit/TestJinjaVariableHasSpaces.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.JinjaVariableHasSpacesRule import JinjaVariableHasSpacesRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestLineTooLong.py b/tests/unit/TestLineTooLong.py
index 07b6169..c1f33c2 100644
--- a/tests/unit/TestLineTooLong.py
+++ b/tests/unit/TestLineTooLong.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.LineTooLongRule import LineTooLongRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestNoIrregularSpacesRule.py b/tests/unit/TestNoIrregularSpacesRule.py
index 320ce8f..f8133b4 100644
--- a/tests/unit/TestNoIrregularSpacesRule.py
+++ b/tests/unit/TestNoIrregularSpacesRule.py
@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.NoIrregularSpacesRule import NoIrregularSpacesRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestNoTabsRule.py b/tests/unit/TestNoTabsRule.py
index ba336ff..3b82101 100644
--- a/tests/unit/TestNoTabsRule.py
+++ b/tests/unit/TestNoTabsRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.NoTabsRule import NoTabsRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestRunner.py b/tests/unit/TestRunner.py
index 122e1bd..b1e8e02 100644
--- a/tests/unit/TestRunner.py
+++ b/tests/unit/TestRunner.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2019 Warpnet B.V.
+# Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
diff --git a/tests/unit/TestSkipRule.py b/tests/unit/TestSkipRule.py
index 269dc67..840c19e 100644
--- a/tests/unit/TestSkipRule.py
+++ b/tests/unit/TestSkipRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.LineTooLongRule import LineTooLongRule
 from saltlint.rules.JinjaVariableHasSpacesRule import JinjaVariableHasSpacesRule
 from tests import RunFromText
diff --git a/tests/unit/TestTrailingWhitespaceRule.py b/tests/unit/TestTrailingWhitespaceRule.py
index 6f54abf..d33cccd 100644
--- a/tests/unit/TestTrailingWhitespaceRule.py
+++ b/tests/unit/TestTrailingWhitespaceRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.TrailingWhitespaceRule import TrailingWhitespaceRule
 from tests import RunFromText
 
diff --git a/tests/unit/TestYamlHasOctalValueRule.py b/tests/unit/TestYamlHasOctalValueRule.py
index 46b0495..b756031 100644
--- a/tests/unit/TestYamlHasOctalValueRule.py
+++ b/tests/unit/TestYamlHasOctalValueRule.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
 # Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
 # Copyright (c) 2018 Ansible by Red Hat
-# Modified work Copyright (c) 2019 Warpnet B.V.
+# Modified work Copyright (c) 2020 Warpnet B.V.
 
 import unittest
 
-from saltlint.linter import RulesCollection
+from saltlint.linter.collection import RulesCollection
 from saltlint.rules.YamlHasOctalValueRule import YamlHasOctalValueRule
 from tests import RunFromText