Repository: sqlfluff/sqlfluff

Issue:
Unnecessary whitespace
As picked up in #501 

Currently the fix for `"SeLEct  *, 1, blah as  fOO  from myTable"` is `"SELECT  *, 1, blah AS  foo  FROM mytable\n"`, which is odd because there's a lot of unnecessary white-space in there.

We already have rules to detect inappropriate indent, and also rules to check _trailing_ whitespace.

This would be a new rule to fix inappropriate whitespace in a query, with the following expections:
- don't lint indents.
- don't lint training whitespace.
- don't lint whitespace which preceeds a comment.

# Examples

## Good

`SELECT * FROM foo`

```
SELECT
    blah,
    foo,      -- With some comment
    bar      -- Another comment
FROM foo
```

## Bad

```
SeLEct  *, 1, blah as  fOO  from myTable
```
...which should correct to
```
SeLEct *, 1, blah as fOO from myTable
```

```
SELECT
    bar                                  AS                                      some_name
FROM                                my_table
```
...should correct to...
```
SELECT
    bar AS some_name
FROM my_table
```



Discussion:

Patch:
diff --git a/src/sqlfluff/core/rules/std/L039.py b/src/sqlfluff/core/rules/std/L039.py
new file mode 100644
index 000000000..4eef46d70
--- /dev/null
+++ b/src/sqlfluff/core/rules/std/L039.py
@@ -0,0 +1,64 @@
+"""Implementation of Rule L039."""
+
+from ..base import BaseCrawler, LintFix, LintResult
+from ..doc_decorators import document_fix_compatible
+
+
+@document_fix_compatible
+class Rule_L039(BaseCrawler):
+    """Unnecessary whitespace found.
+
+    | **Anti-pattern**
+
+    .. code-block::
+
+        SELECT
+            a,        b
+        FROM foo
+
+    | **Best practice**
+    | Unless an indent or preceeding a comment, whitespace should
+    be a single space.
+
+    .. code-block::
+
+        SELECT
+            a, b
+        FROM foo
+    """
+
+    def _eval(self, segment, parent_stack, **kwargs):
+        """Unnecessary whitespace."""
+        # For the given segment, lint whitespace directly within it.
+        prev_newline = True
+        prev_whitespace = None
+        for seg in segment.segments:
+            if seg.is_type("newline"):
+                prev_newline = True
+                prev_whitespace = None
+            elif seg.is_type("whitespace"):
+                # This is to avoid indents
+                if not prev_newline:
+                    prev_whitespace = seg
+                prev_newline = False
+            elif seg.is_type("comment"):
+                prev_newline = False
+                prev_whitespace = None
+            else:
+                if prev_whitespace:
+                    if prev_whitespace.raw != " ":
+                        return LintResult(
+                            anchor=prev_whitespace,
+                            fixes=[
+                                LintFix(
+                                    "edit",
+                                    prev_whitespace,
+                                    self.make_whitespace(
+                                        raw=" ", pos_marker=prev_whitespace.pos_marker
+                                    ),
+                                )
+                            ],
+                        )
+                prev_newline = False
+                prev_whitespace = None
+        return None


Test patch:
diff --git a/test/api/simple_test.py b/test/api/simple_test.py
index 7aa93e24a..a47e90465 100644
--- a/test/api/simple_test.py
+++ b/test/api/simple_test.py
@@ -21,18 +21,36 @@ lint_result = [
         "line_no": 1,
         "line_pos": 1,
     },
+    {
+        "code": "L039",
+        "description": "Unnecessary whitespace found.",
+        "line_no": 1,
+        "line_pos": 7,
+    },
     {
         "code": "L013",
         "line_no": 1,
         "line_pos": 12,
         "description": "Column expression without alias. Use explicit `AS` clause.",
     },
+    {
+        "code": "L039",
+        "description": "Unnecessary whitespace found.",
+        "line_no": 1,
+        "line_pos": 22,
+    },
     {
         "code": "L014",
         "line_no": 1,
         "line_pos": 24,
         "description": "Inconsistent capitalisation of unquoted identifiers.",
     },
+    {
+        "code": "L039",
+        "description": "Unnecessary whitespace found.",
+        "line_no": 1,
+        "line_pos": 27,
+    },
     {
         "code": "L009",
         "line_no": 1,
@@ -80,7 +98,7 @@ def test__api__fix_string():
     # Check return types.
     assert isinstance(result, str)
     # Check actual result
-    assert result == "SELECT  *, 1, blah AS  foo  FROM mytable\n"
+    assert result == "SELECT *, 1, blah AS foo FROM mytable\n"
 
 
 def test__api__fix_string_specific():
diff --git a/test/cli/commands_test.py b/test/cli/commands_test.py
index 80b7ea858..175abbbc6 100644
--- a/test/cli/commands_test.py
+++ b/test/cli/commands_test.py
@@ -160,7 +160,7 @@ def test__cli__command_lint_stdin(command):
             [
                 "-n",
                 "--exclude-rules",
-                "L006,L007,L031",
+                "L006,L007,L031,L039",
                 "test/fixtures/linter/operator_errors.sql",
             ],
         ),
diff --git a/test/core/rules/test_cases/L039.yml b/test/core/rules/test_cases/L039.yml
new file mode 100644
index 000000000..46abd9251
--- /dev/null
+++ b/test/core/rules/test_cases/L039.yml
@@ -0,0 +1,18 @@
+rule: L039
+
+test_basic:
+  pass_str: SELECT 1
+
+test_basic_fix:
+  fail_str: SELECT     1
+  fix_str: SELECT 1
+
+test_simple_fix:
+  fail_str: |
+    select
+        1 + 2     + 3     + 4        -- Comment
+    from     foo
+  fix_str: |
+    select
+        1 + 2 + 3 + 4        -- Comment
+    from foo