Repository: alanmcruickshank/sqlfluff

Issue:
"Whitespace around operators" check reports the same warning twice
Query:
```
SELECT
    a/b AS click_rate
FROM
    t
```

Output:
```
$ sqlfluff lint test.sql
== [test.sql] FAIL
L:   2 | P:   6 | L006 | Operators should be surrounded by a single whitespace.
L:   2 | P:   7 | L006 | Operators should be surrounded by a single whitespace.
```

Note the warning is correct, but it is reported twice -- apparently once on the operator and once on the second operand.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75d6c01c..e27f4c91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,27 +5,6 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased]
-
-### Added
-
-- A `--code-only` option to the `parse` command to spit out a more
-  simplified output with only the code elements.
-
-### Changed
-
-- Bugfix, correct missing files in `setup.py` `install_requires` section.
-- Better parsing of the *not equal* operator.
-- Added more exclusions to identifier reserved words to fix cross joins.
-- At verbosity levels 2 or above, the root config is printed and then any
-  diffs to that for specific files are also printed.
-
-## [0.2.3] - 2019-12-02
-
-### Changed
-
-- Bugfix, default config not included.
-
 ## [0.2.2] - 2019-12-02
 
 ### Changed
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index 5588eaef..6b7bc8bb 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -1,5 +1,3 @@
-.. _config:
-
 Configuration
 =============
 
@@ -31,27 +29,15 @@ Nesting
 -------
 
 **Sqlfluff** uses **nesting** in it's configuration files, with files
-closer *overriding* (or *patching*, if you will) values from other files.
-That means you'll end up with a final config which will be a patchwork
-of all the values from the config files loaded up to that path.
-You don't **need** any config files to be present to make *sqlfluff*
-work. If you do want to override any values though sqlfluff will use
-files in the following locations in order, with values from later
-steps overriding those from earlier:
-
-0. *[...and this one doesn't really count]* There's a default config as
-   part of the sqlfluff package. You can find this below, in the
-   :ref:`defaultconfig` section.
-1. It will look in the user's home directory (~), for any of the
-   filenames above in the main :ref:`config` section. If
-   multiple are present, they will *patch*/*override* eachother
-   in the order above.
-2. It will look for the same files in the current working directory.
-3. *[if parsing a file in a subdirectory of the current working directory]*
-   It will look for the same files in every subdirectory between the
-   current working dir and the file directory.
-4. It will look for the same files in the directory containing the file
-   being linted.
+closer to the file being parsed being favoured over other config. The
+following locations are checked:
+
+1. The current user's home directory
+2. The current working directory
+3. *(if the file in question is in a subpath of the current working*
+   *directory)* every directory in between
+4. Any configuration passed at the command line is then used as a final
+    overlay.
 
 This whole structure leads to efficient configuration, in particular
 in projects which utilise a lot of complicated templating.
@@ -179,7 +165,6 @@ projects. In particular it provides mock objects for:
 .. _`dbt`: https://www.getdbt.com/
 .. _`github`: https://www.github.com/alanmcruickshank/sqlfluff
 
-.. _defaultconfig:
 
 Default Configuration
 ---------------------
diff --git a/setup.py b/setup.py
index 637e47d6..fad5617e 100644
--- a/setup.py
+++ b/setup.py
@@ -75,10 +75,7 @@ setup(
     ],
     install_requires=[
         'click>=2.0',
-        'six>=1.0',
-        'configparser',
-        'oyaml',
-        'Jinja2'
+        'six>=1.0'
     ],
     extras_require={
         # eg:
@@ -91,5 +88,5 @@ setup(
         ]
     },
     # Use datafiles to make sure the config versioning file is included
-    data_files=[('', ['src/sqlfluff/config.ini', 'README.md', 'CHANGELOG.md', 'src/sqlfluff/default_config.cfg'])]
+    data_files=[('', ['src/sqlfluff/config.ini', 'README.md', 'CHANGELOG.md'])]
 )
diff --git a/src/sqlfluff/__init__.py b/src/sqlfluff/__init__.py
index 575339ed..81e087bd 100644
--- a/src/sqlfluff/__init__.py
+++ b/src/sqlfluff/__init__.py
@@ -1,9 +1,11 @@
 """Sqlfluff is a SQL linter for humans."""
+
+import configparser
 import pkg_resources
-from six.moves import configparser
+
 
 # Get the current version
 config = configparser.ConfigParser()
-config.read([pkg_resources.resource_filename('sqlfluff', 'config.ini')])
+config.read_file(open(pkg_resources.resource_filename('sqlfluff', 'config.ini')))
 
 __version__ = config.get('sqlfluff', 'version')
diff --git a/src/sqlfluff/cli/commands.py b/src/sqlfluff/cli/commands.py
index 8a8d3666..72c57709 100644
--- a/src/sqlfluff/cli/commands.py
+++ b/src/sqlfluff/cli/commands.py
@@ -126,6 +126,9 @@ def lint(paths, **kwargs):
     config_string = format_config(lnt, verbose=verbose)
     if len(config_string) > 0:
         lnt.log(config_string)
+    # Lint the paths
+    if verbose > 1:
+        lnt.log("==== logging ====")
     # add stdin if specified via lone '-'
     if ('-',) == paths:
         result = lnt.lint_string_wrapped(sys.stdin.read(), fname='stdin', verbosity=verbose)
@@ -210,9 +213,7 @@ def fix(force, paths, **kwargs):
 @common_options
 @click.argument('path', nargs=1)
 @click.option('--recurse', default=0, help='The depth to recursively parse to (0 for unlimited)')
-@click.option('-c', '--code-only', is_flag=True,
-              help='Output only the code elements of the parse tree.')
-def parse(path, code_only, **kwargs):
+def parse(path, **kwargs):
     """Parse SQL files and just spit out the result.
 
     PATH is the path to a sql file or directory to lint. This can be either a
@@ -229,15 +230,12 @@ def parse(path, code_only, **kwargs):
     if len(config_string) > 0:
         lnt.log(config_string)
 
-    # TODO: do this better
-    print(c)
-
     nv = 0
     try:
         # A single path must be specified for this command
         for parsed, violations, time_dict in lnt.parse_path(path, verbosity=verbose, recurse=recurse):
             if parsed:
-                lnt.log(parsed.stringify(code_only=code_only))
+                lnt.log(parsed.stringify())
             else:
                 # TODO: Make this prettier
                 lnt.log('...Failed to Parse...')
diff --git a/src/sqlfluff/cli/formatters.py b/src/sqlfluff/cli/formatters.py
index abdd3b79..700723a5 100644
--- a/src/sqlfluff/cli/formatters.py
+++ b/src/sqlfluff/cli/formatters.py
@@ -3,18 +3,15 @@
 
 from six import StringIO
 
-from .helpers import colorize, cli_table, get_package_version, get_python_version, pad_line
+from .helpers import colorize, cli_table, get_package_version, get_python_version
 from ..errors import SQLBaseError
 
 
 def format_filename(filename, success=False, verbose=0, success_text='PASS'):
     """Format filenames."""
-    if isinstance(success, str):
-        status_string = success
-    else:
-        status_string = colorize(
-            success_text if success else 'FAIL',
-            'green' if success else 'red')
+    status_string = colorize(
+        success_text if success else 'FAIL',
+        'green' if success else 'red')
     return (
         "== ["
         + colorize("{0}".format(filename), 'lightgrey')
@@ -177,15 +174,6 @@ def format_linting_result(result, verbose=0):
     return text_buffer.getvalue()
 
 
-def format_config_vals(config_vals):
-    """Format an iterable of config values from a config object."""
-    text_buffer = StringIO()
-    for i, k, v in config_vals:
-        val = '' if v is None else str(v)
-        text_buffer.write(("    " * i) + colorize(pad_line(str(k) + ':', 20, 'left'), color='lightgrey') + pad_line(val, 20, 'left') + '\n')
-    return text_buffer.getvalue()
-
-
 def format_config(linter, verbose=0):
     """Format the config of a `Linter`."""
     text_buffer = StringIO()
@@ -202,9 +190,6 @@ def format_config(linter, verbose=0):
         text_buffer.write("\n")
         if linter.config.get('rule_whitelist'):
             text_buffer.write(cli_table([('rules', ', '.join(linter.config.get('rule_whitelist')))], col_width=41))
-        if verbose > 1:
-            text_buffer.write("== Raw Config:\n")
-            text_buffer.write(format_config_vals(linter.config.iter_vals()))
     return text_buffer.getvalue()
 
 
diff --git a/src/sqlfluff/config.ini b/src/sqlfluff/config.ini
index aa75ec71..c29e20df 100644
--- a/src/sqlfluff/config.ini
+++ b/src/sqlfluff/config.ini
@@ -1,3 +1,3 @@
 # Config file for sqlfluff, mostly for version info for now
 [sqlfluff]
-version=0.2.3
+version=0.2.2
diff --git a/src/sqlfluff/config.py b/src/sqlfluff/config.py
index 8a955c5f..db4c87b2 100644
--- a/src/sqlfluff/config.py
+++ b/src/sqlfluff/config.py
@@ -1,9 +1,7 @@
 """Module for loading config."""
 
+import configparser
 import os
-import sys
-
-from six.moves import configparser
 
 from .dialects import dialect_selector
 from .templaters import templater_selector
@@ -109,10 +107,7 @@ class ConfigLoader(object):
         """
         buff = []
         # Disable interpolation so we can load macros
-        kw = {}
-        if sys.version_info >= (3, 0):
-            kw['interpolation'] = None
-        config = configparser.ConfigParser(**kw)
+        config = configparser.ConfigParser(interpolation=None)
         config.read(fpath)
         for k in config.sections():
             if k == 'sqlfluff':
@@ -254,7 +249,6 @@ class ConfigLoader(object):
 
 class FluffConfig(object):
     """.The class that actually gets passed around as a config object."""
-    private_vals = ['rule_blacklist', 'rule_whitelist', 'dialect_obj', 'templater_obj']
 
     def __init__(self, configs=None, overrides=None):
         self._overrides = overrides  # We only store this for child configs
@@ -338,28 +332,3 @@ class FluffConfig(object):
                 if buff is None:
                     return None
             return buff
-
-    def iter_vals(self, cfg=None):
-        """Return an iterable of tuples representing keys.
-
-        We show values before dicts, the tuple contains an indent
-        value to know what level of the dict we're in. Dict labels
-        will be returned as a blank value before their content.
-        """
-        cfg = cfg or self._configs
-
-        # Get keys and sort
-        keys = sorted(cfg.keys())
-        # First iterate values (alphabetically):
-        for k in keys:
-            if not isinstance(cfg[k], dict) and cfg[k] is not None and k not in self.private_vals:
-                yield (0, k, cfg[k])
-
-        # Then iterate dicts (alphabetically (but `core` comes first if it exists))
-        for k in keys:
-            if isinstance(cfg[k], dict):
-                # First yield the dict label
-                yield (0, k, '')
-                # Then yield it's content
-                for idnt, key, val in self.iter_vals(cfg=cfg[k]):
-                    yield (idnt + 1, key, val)
diff --git a/src/sqlfluff/dialects/dialect_ansi.py b/src/sqlfluff/dialects/dialect_ansi.py
index 9c5f06f0..4862ecbf 100644
--- a/src/sqlfluff/dialects/dialect_ansi.py
+++ b/src/sqlfluff/dialects/dialect_ansi.py
@@ -31,11 +31,11 @@ ansi_dialect.set_lexer_struct([
     ("double_quote", "regex", r'"[^"]*"', dict(is_code=True)),
     ("back_quote", "regex", r"`[^`]*`", dict(is_code=True)),
     ("numeric_literal", "regex", r"([0-9]+(\.[0-9]+)?)", dict(is_code=True)),
-    ("not_equal", "regex", r"!=|<>", dict(is_code=True)),
     ("greater_than_or_equal", "regex", r">=", dict(is_code=True)),
     ("less_than_or_equal", "regex", r"<=", dict(is_code=True)),
     ("newline", "regex", r"\r\n", None),
     ("casting_operator", "regex", r"::", dict(is_code=True)),
+    ("not_equals", "regex", r"!=", dict(is_code=True)),
     ("newline", "singleton", "\n", None),
     ("equals", "singleton", "=", dict(is_code=True)),
     ("greater_than", "singleton", ">", dict(is_code=True)),
@@ -75,13 +75,11 @@ ansi_dialect.add(
     LessThanSegment=KeywordSegment.make('<', name='less_than', type='comparison_operator'),
     GreaterThanOrEqualToSegment=KeywordSegment.make('>=', name='greater_than_equal_to', type='comparison_operator'),
     LessThanOrEqualToSegment=KeywordSegment.make('<=', name='less_than_equal_to', type='comparison_operator'),
-    NotEqualToSegment_a=KeywordSegment.make('!=', name='not_equal_to', type='comparison_operator'),
-    NotEqualToSegment_b=KeywordSegment.make('<>', name='not_equal_to', type='comparison_operator'),
     # The strange regex here it to make sure we don't accidentally match numeric literals. We
     # also use a regex to explicitly exclude disallowed keywords.
     NakedIdentifierSegment=ReSegment.make(
         r"[A-Z0-9_]*[A-Z][A-Z0-9_]*", name='identifier', type='naked_identifier',
-        _anti_template=r"(JOIN|ON|USING|CROSS|INNER|LEFT|RIGHT|OUTER)"),
+        _anti_template=r"(JOIN|ON|USING)"),
     FunctionNameSegment=ReSegment.make(r"[A-Z][A-Z0-9_]*", name='function_name', type='function_name'),
     # Maybe data types should be more restrictive?
     DatatypeSegment=ReSegment.make(r"[A-Z][A-Z0-9_]*", name='data_type', type='data_type'),
@@ -101,8 +99,7 @@ ansi_dialect.add(
         Ref('AndKeywordSegment'), Ref('OrKeywordSegment')),
     ComparisonOperatorGrammar=OneOf(
         Ref('EqualsSegment'), Ref('GreaterThanSegment'), Ref('LessThanSegment'),
-        Ref('GreaterThanOrEqualToSegment'), Ref('LessThanOrEqualToSegment'),
-        Ref('NotEqualToSegment_a'), Ref('NotEqualToSegment_b')),
+        Ref('GreaterThanOrEqualToSegment'), Ref('LessThanOrEqualToSegment')),
     # Keywords
     AsKeywordSegment=KeywordSegment.make('as'),
     FromKeywordSegment=KeywordSegment.make('from'),
diff --git a/src/sqlfluff/errors.py b/src/sqlfluff/errors.py
index 8400feab..f75dec43 100644
--- a/src/sqlfluff/errors.py
+++ b/src/sqlfluff/errors.py
@@ -21,9 +21,14 @@ class SQLBaseError(ValueError):
 
         NB: For violations which don't directly implement a rule
         this attempts to return the error message linked to whatever
-        caused the violation.
+        caused the violation. Optionally some errors may have their
+        description set directly.
         """
-        if hasattr(self, 'rule'):
+        if hasattr(self, 'description') and self.description:
+            # This can only override if it's present AND
+            # if it's non-null.
+            return self.description
+        elif hasattr(self, 'rule'):
             return self.rule.description
         else:
             # Return the first element - probably a string message
@@ -148,6 +153,7 @@ class SQLLintError(SQLBaseError):
         self.segment = kwargs.pop('segment', None)
         self.rule = kwargs.pop('rule', None)
         self.fixes = kwargs.pop('fixes', [])
+        self.description = kwargs.pop('description', None)
         super(SQLLintError, self).__init__(*args, **kwargs)
 
     def check_tuple(self):
diff --git a/src/sqlfluff/linter.py b/src/sqlfluff/linter.py
index 8ca0a2d7..18ab702d 100644
--- a/src/sqlfluff/linter.py
+++ b/src/sqlfluff/linter.py
@@ -12,8 +12,7 @@ from .parser.segments_base import verbosity_logger, frame_msg, ParseContext
 from .rules import get_ruleset
 
 
-from .cli.formatters import (format_linting_path, format_file_violations,
-                             format_filename, format_config_vals)
+from .cli.formatters import format_linting_path, format_file_violations
 
 
 class LintedFile(namedtuple('ProtoFile', ['path', 'violations', 'time_dict', 'tree', 'file_mask'])):
@@ -375,30 +374,19 @@ class Linter(object):
         """Parse a string.
 
         Returns:
-            `tuple` of (`parsed`, `violations`, `time_dict`, `config_diff`).
+            `tuple` of (`parsed`, `violations`, `time_dict`).
                 `parsed` is a segment structure representing the parsed file. If
                     parsing fails due to an inrecoverable violation then we will
                     return None.
-                `violations` is a :obj:`list` of violations so far, which will either be
+                `violations` is a list of violations so far, which will either be
                     templating, lexing or parsing violations at this stage.
-                `time_dict` is a :obj:`dict` containing timings for how long each step
+                `time_dict` is a dict containing timings for how long each step
                     took in the process.
 
         """
         violations = []
         t0 = get_time()
 
-        # Log the start of this process if we're in a more verbose mode.
-        if verbosity > 1:
-            self.log(format_filename(filename=fname, success='PARSING', verbose=verbosity))
-            # This is where we output config diffs if they exist.
-            if config:
-                # Only output config diffs if there is a config to diff to.
-                config_diff = config.diff_to(self.config)
-                if config_diff:
-                    self.log("   Config Diff:")
-                    self.log(format_config_vals(self.config.iter_vals(cfg=config_diff)))
-
         verbosity_logger("TEMPLATING RAW [{0}] ({1})".format(self.templater.name, fname), verbosity=verbosity)
         try:
             s = self.templater.process(s, fname=fname, config=config or self.config)
@@ -544,11 +532,9 @@ class Linter(object):
             fixed_buff = parsed.raw
 
         file_mask = (raw_buff, templ_buff, fixed_buff)
-        res = LintedFile(fname, vs, time_dict, parsed,
-                         file_mask=file_mask)
+        res = LintedFile(fname, vs, time_dict, parsed, file_mask)
         # Do the logging as appropriate (don't log if fixing...)
         if not fix:
-            # This is the main command line output from linting.
             self.log(format_file_violations(fname, res.violations, verbose=verbosity))
         return res
 
@@ -558,17 +544,17 @@ class Linter(object):
             raise IOError("Specified path does not exist")
         elif os.path.isdir(path):
             # Then expand the path!
-            buffer = []
+            buffer = set()
             for dirpath, _, filenames in os.walk(path):
                 for fname in filenames:
                     for ext in self.sql_exts:
                         # is it a sql file?
                         if fname.endswith(ext):
                             # join the paths and normalise
-                            buffer.append(os.path.normpath(os.path.join(dirpath, fname)))
-            return sorted(buffer)
+                            buffer.add(os.path.normpath(os.path.join(dirpath, fname)))
+            return buffer
         else:
-            return [path]
+            return set([path])
 
     def lint_string_wrapped(self, string, fname='<string input>', verbosity=0, fix=False):
         """Lint strings directly."""
@@ -587,9 +573,7 @@ class Linter(object):
         for fname in self.paths_from_path(path):
             config = self.config.make_child_from_path(fname)
             with open(fname, 'r') as f:
-                linted_path.add(
-                    self.lint_string(f.read(), fname=fname, verbosity=verbosity,
-                                     fix=fix, config=config))
+                linted_path.add(self.lint_string(f.read(), fname=fname, verbosity=verbosity, fix=fix, config=config))
         return linted_path
 
     def lint_paths(self, paths, verbosity=0, fix=False):
diff --git a/src/sqlfluff/parser/segments_base.py b/src/sqlfluff/parser/segments_base.py
index d7f948e2..5ec226cd 100644
--- a/src/sqlfluff/parser/segments_base.py
+++ b/src/sqlfluff/parser/segments_base.py
@@ -366,28 +366,23 @@ class BaseSegment(object):
         """Returns only the non-comment elements of this segment."""
         return [seg for seg in self.segments if seg.type != 'comment']
 
-    def stringify(self, ident=0, tabsize=4, pos_idx=60, raw_idx=80, code_only=False):
+    def stringify(self, ident=0, tabsize=4, pos_idx=60, raw_idx=80):
         """Use indentation to render this segment and it's children as a string."""
         buff = StringIO()
         preface = self._preface(ident=ident, tabsize=tabsize, pos_idx=pos_idx, raw_idx=raw_idx)
         buff.write(preface + '\n')
-        if not code_only and self.comment_seperate and len(self._comments) > 0:
+        if self.comment_seperate and len(self._comments) > 0:
             if self._comments:
                 buff.write((' ' * ((ident + 1) * tabsize)) + 'Comments:' + '\n')
                 for seg in self._comments:
-                    buff.write(seg.stringify(ident=ident + 2, tabsize=tabsize, pos_idx=pos_idx,
-                                             raw_idx=raw_idx, code_only=code_only))
+                    buff.write(seg.stringify(ident=ident + 2, tabsize=tabsize, pos_idx=pos_idx, raw_idx=raw_idx))
             if self._non_comments:
                 buff.write((' ' * ((ident + 1) * tabsize)) + 'Code:' + '\n')
                 for seg in self._non_comments:
-                    buff.write(seg.stringify(ident=ident + 2, tabsize=tabsize, pos_idx=pos_idx,
-                                             raw_idx=raw_idx, code_only=code_only))
+                    buff.write(seg.stringify(ident=ident + 2, tabsize=tabsize, pos_idx=pos_idx, raw_idx=raw_idx))
         else:
             for seg in self.segments:
-                # If we're in code_only, only show the code segments, otherwise always true
-                if not code_only or seg.is_code:
-                    buff.write(seg.stringify(ident=ident + 1, tabsize=tabsize, pos_idx=pos_idx,
-                                             raw_idx=raw_idx, code_only=code_only))
+                buff.write(seg.stringify(ident=ident + 1, tabsize=tabsize, pos_idx=pos_idx, raw_idx=raw_idx))
         return buff.getvalue()
 
     @staticmethod
@@ -777,7 +772,7 @@ class RawSegment(BaseSegment):
             self.pos_marker,
             self.raw)
 
-    def stringify(self, ident=0, tabsize=4, pos_idx=60, raw_idx=80, code_only=False):
+    def stringify(self, ident=0, tabsize=4, pos_idx=60, raw_idx=80):
         """Use indentation to render this segment and it's children as a string."""
         preface = self._preface(ident=ident, tabsize=tabsize, pos_idx=pos_idx, raw_idx=raw_idx)
         return preface + '\n'
diff --git a/src/sqlfluff/rules/base.py b/src/sqlfluff/rules/base.py
index 67e4d35e..01f7349e 100644
--- a/src/sqlfluff/rules/base.py
+++ b/src/sqlfluff/rules/base.py
@@ -37,21 +37,29 @@ class LintResult(object):
         memory (:obj:`dict`, optional): An object which stores any working
             memory for the crawler. The `memory` returned in any `LintResult`
             will be passed as an input to the next segment to be crawled.
+        description (:obj:`str`, optional): A description of the problem
+            identified as part of this result. This will override the
+            description of the rule as what gets reported to the user
+            with the problem if provided.
 
     """
 
-    def __init__(self, anchor=None, fixes=None, memory=None):
+    def __init__(self, anchor=None, fixes=None, memory=None, description=None):
         # An anchor of none, means no issue
         self.anchor = anchor
         # Fixes might be blank
         self.fixes = fixes or []
         # Memory is passed back in the linting result
         self.memory = memory
+        # store a description_override for later
+        self.description = description
 
     def to_linting_error(self, rule):
         """Convert a linting result to a :exc:`SQLLintError` if appropriate."""
         if self.anchor:
-            return SQLLintError(rule=rule, segment=self.anchor, fixes=self.fixes)
+            # Allow description override from the LintRestult
+            description = self.description or rule.description
+            return SQLLintError(rule=rule, segment=self.anchor, fixes=self.fixes, description=description)
         else:
             return None
 
diff --git a/src/sqlfluff/rules/std.py b/src/sqlfluff/rules/std.py
index 18985943..6d1b2c1f 100644
--- a/src/sqlfluff/rules/std.py
+++ b/src/sqlfluff/rules/std.py
@@ -241,6 +241,7 @@ class Rule_L006(BaseCrawler):
         # anchor is our signal as to whether there's a problem
         anchor = None
         fixes = []
+        description = None
 
         # The parent stack tells us whether we're in an expression or not.
         if parent_stack and parent_stack[-1].type == 'expression':
@@ -251,6 +252,8 @@ class Rule_L006(BaseCrawler):
                     anchor, fixes = _handle_previous_segments(
                         memory['since_code'], anchor=segment, this_segment=segment,
                         fixes=fixes)
+                    if anchor:
+                        description = "Operators should be preceded by a space."
                 else:
                     # It's not an operator, we can evaluate what happened after an
                     # operator if that's the last code we saw.
@@ -259,6 +262,8 @@ class Rule_L006(BaseCrawler):
                         anchor, fixes = _handle_previous_segments(
                             memory['since_code'], anchor=memory['last_code'],
                             this_segment=segment, fixes=fixes)
+                        if anchor:
+                            description = "Operators should be followed by a space."
                     else:
                         # This isn't an operator, and the thing before it wasn't
                         # either. I don't think that's an issue for now.
@@ -276,7 +281,7 @@ class Rule_L006(BaseCrawler):
 
         # Anchor is our signal as to whether there's a problem
         if anchor:
-            return LintResult(anchor=anchor, memory=memory, fixes=fixes)
+            return LintResult(anchor=anchor, memory=memory, fixes=fixes, description=description)
         else:
             return LintResult(memory=memory)
 
diff --git a/src/sqlfluff/templaters.py b/src/sqlfluff/templaters.py
index 41b939ce..f4ba0824 100644
--- a/src/sqlfluff/templaters.py
+++ b/src/sqlfluff/templaters.py
@@ -69,13 +69,6 @@ class RawTemplateInterface(object):
         """
         return in_str
 
-    def __eq__(self, other):
-        """Return true if `other` is of the same class as this one.
-
-        NB: This is useful in comparing configs.
-        """
-        return isinstance(other, self.__class__)
-
 
 @register_templater
 class PythonTemplateInterface(RawTemplateInterface):


Test patch:
diff --git a/test/cli_commands_test.py b/test/cli_commands_test.py
index 99f3109f..f93e3a9e 100644
--- a/test/cli_commands_test.py
+++ b/test/cli_commands_test.py
@@ -73,25 +73,34 @@ def test__cli__command_lint_stdin(command):
     invoke_assert_code(args=[lint, command], kwargs=dict(input=sql))
 
 
-@pytest.mark.parametrize('command', [
-    # Test basic linting
-    (lint, ['-n', 'test/fixtures/cli/passing_b.sql']),
-    # Check basic parsing
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql']),
-    # Check basic parsing, with the code only option
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-c']),
-    # Check linting works in specifying rules
-    (lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works in specifying multiple rules
-    (lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works with both included and excluded rules
-    (lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works with just excluded rules
-    (lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql'])
-])
-def test__cli__command_lint_b(command):
+def test__cli__command_lint_b():
     """Check basic commands on a more complicated script."""
-    invoke_assert_code(args=command)
+    invoke_assert_code(args=[lint, ['-n', 'test/fixtures/cli/passing_b.sql']])
+
+
+def test__cli__command_parse():
+    """Check parse command."""
+    invoke_assert_code(args=[parse, ['-n', 'test/fixtures/cli/passing_b.sql']])
+
+
+def test__cli__command_lint_c_rules_single():
+    """Check that only checking for a single specific rule using the cli works."""
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql']])
+
+
+def test__cli__command_lint_c_rules_multi():
+    """Check that only checking for multiple specific rules using the cli works."""
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql']])
+
+
+def test__cli__command_lint_c_exclude_rules_single():
+    """Check that only excluding a single specific rule using the cli works."""
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql']])
+
+
+def test__cli__command_lint_c_exclude_rules_multi():
+    """Check that excluding multiple specific rules using the cli works."""
+    invoke_assert_code(args=[lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql']])
 
 
 def test__cli__command_versioning():
diff --git a/test/cli_formatters_test.py b/test/cli_formatters_test.py
index 84cad892..ca6f55d9 100644
--- a/test/cli_formatters_test.py
+++ b/test/cli_formatters_test.py
@@ -45,20 +45,21 @@ def test__cli__formatters__violations():
         'foo': [
             SQLLintError(
                 segment=RawSegment('blah', FilePositionMarker(0, 25, 2, 26)),
-                rule=RuleGhost('A', 'DESC')),
+                rule=RuleGhost('A', 'DESCR')),
+            # Here we check the optional description override
             SQLLintError(
                 segment=RawSegment('blah', FilePositionMarker(0, 21, 3, 22)),
-                rule=RuleGhost('B', 'DESC'))],
+                rule=RuleGhost('B', 'DESCR'), description='foo')],
         'bar': [
             SQLLintError(
                 segment=RawSegment('blah', FilePositionMarker(0, 2, 11, 3)),
-                rule=RuleGhost('C', 'DESC'))]
+                rule=RuleGhost('C', 'DESCR'))]
     }
     f = format_path_violations(v)
     k = sorted(['foo', 'bar'])
     chk = {
-        'foo': ["L:  21 | P:   3 | B | DESC", "L:  25 | P:   2 | A | DESC"],
-        'bar': ["L:   2 | P:  11 | C | DESC"]
+        'foo': ["L:  21 | P:   3 | B | foo", "L:  25 | P:   2 | A | DESCR"],
+        'bar': ["L:   2 | P:  11 | C | DESCR"]
     }
     chk2 = []
     for elem in k:
diff --git a/test/fixtures/parser/ansi/select_simple_j.sql b/test/fixtures/parser/ansi/select_simple_j.sql
deleted file mode 100644
index 22eb0d31..00000000
--- a/test/fixtures/parser/ansi/select_simple_j.sql
+++ /dev/null
@@ -1,7 +0,0 @@
--- test parsing of cross join
-SELECT
-    count_correctly_substituted
-FROM
-    correctly_substituted
-CROSS JOIN
-    needs_substitution
diff --git a/test/fixtures/parser/ansi/select_simple_j.yml b/test/fixtures/parser/ansi/select_simple_j.yml
deleted file mode 100644
index e1b6eb29..00000000
--- a/test/fixtures/parser/ansi/select_simple_j.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Test that casting works as expected
-file:
-  statement:
-    select_statement:
-      keyword: SELECT
-      select_target_group:
-        select_target_element:
-          object_reference:
-            naked_identifier: count_correctly_substituted
-      from_clause:
-        keyword: FROM
-        table_expression:
-          object_reference:
-            naked_identifier: correctly_substituted
-        join_clause:
-        - keyword: CROSS
-        - keyword: JOIN
-        - table_expression:
-            object_reference:
-              naked_identifier: needs_substitution