Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Rule L052: Semi colon alignment #1902

Merged
merged 22 commits into from Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f055db6
Add L052: Statements must end with a semi-colon
jpy-git Nov 11, 2021
9270e57
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 14, 2021
f511c02
Address unit tests - TODO: address L016 interaction case
jpy-git Nov 14, 2021
aef7475
[many dialects] Implement generic placeholder templating (#1887)
jacopofar Nov 14, 2021
63b17a3
Add Tox publish commands (#1853)
jpy-git Nov 14, 2021
541e39a
L036 docstring refinements (#1903)
jpy-git Nov 14, 2021
2596627
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 14, 2021
ed55202
Backing out original unit test changes
jpy-git Nov 14, 2021
c422220
Merge branch 'semi_colon_alignment' of https://github.com/jpy-git/sql…
jpy-git Nov 14, 2021
6977c65
Update code/docs/test to reflect trailing semi-colon not being enforc…
jpy-git Nov 14, 2021
3605cc4
Add ability to select whether semi-colons are on the same or a new line
jpy-git Nov 14, 2021
3afb4d2
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 14, 2021
adc4928
Adding missing test case scenario
jpy-git Nov 14, 2021
b21c497
Merge branch 'semi_colon_alignment' of https://github.com/jpy-git/sql…
jpy-git Nov 14, 2021
bc60d61
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 16, 2021
bf642ca
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 17, 2021
66ed118
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 17, 2021
f9b92e7
implement code review feedback
jpy-git Nov 18, 2021
8dcb9f1
Merge branch 'main' into semi_colon_alignment
WittierDinosaur Nov 18, 2021
0790689
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 18, 2021
1922203
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 18, 2021
3b2892b
Merge branch 'main' into semi_colon_alignment
jpy-git Nov 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/sqlfluff/core/default_config.cfg
Expand Up @@ -89,3 +89,7 @@ forbid_subquery_in = join
[sqlfluff:rules:L047] # Consistent syntax to count all rows
prefer_count_1 = False
prefer_count_0 = False

[sqlfluff:rules:L052] # Semi-colon formatting approach.
semicolon_newline = False
require_final_semicolon = False
14 changes: 14 additions & 0 deletions src/sqlfluff/core/rules/config_info.py
Expand Up @@ -103,6 +103,20 @@
"Should alias have an explict AS or is implicit aliasing required?"
),
},
"semicolon_newline": {
"validation": [True, False],
"definition": (
"Should semi-colons be placed on a new line after the statement end?"
),
},
"require_final_semicolon": {
"validation": [True, False],
"definition": (
"Should final semi-colons be required? "
"(N.B. forcing trailing semi-colons is not recommended for dbt users "
"as it can cause issues when wrapping the query within other SQL queries)"
),
},
}


Expand Down
173 changes: 173 additions & 0 deletions src/sqlfluff/rules/L052.py
@@ -0,0 +1,173 @@
"""Implementation of Rule L052."""
from typing import List, Optional

from sqlfluff.core.parser import SymbolSegment
from sqlfluff.core.parser.segments.base import BaseSegment
from sqlfluff.core.parser.segments.raw import NewlineSegment

from sqlfluff.core.rules.base import BaseRule, LintResult, LintFix, RuleContext
from sqlfluff.core.rules.doc_decorators import (
document_configuration,
document_fix_compatible,
)


@document_configuration
@document_fix_compatible
class Rule_L052(BaseRule):
"""Statements must end with a semi-colon.

| **Anti-pattern**
| A statement is not immediately terminated with a semi-colon, the • represents space.

.. code-block:: sql
:force:

SELECT
a
FROM foo

;

SELECT
b
FROM bar••;

| **Best practice**
| Immediately terminate the statement with a semi-colon.

.. code-block:: sql
:force:

SELECT
a
FROM foo;
"""

config_keywords = ["semicolon_newline", "require_final_semicolon"]

def _eval(self, context: RuleContext) -> Optional[LintResult]:
"""Statements must end with a semi-colon."""
# Config type hints
self.semicolon_newline: bool
self.require_final_semicolon: bool

# First we can simply handle the case of existing semi-colon alignment.
whitespace_set = {"newline", "whitespace"}
if context.segment.name == "semicolon":

# Locate semicolon and iterate back over the raw stack to find
# whitespace between the semi-colon and the end of the preceding statement.
anchor_segment = context.segment
whitespace_deletions = []
for segment in context.raw_stack[::-1]:
if segment.name not in whitespace_set:
break
whitespace_deletions.append(segment)
anchor_segment = segment

fixes: List[LintFix] = []
# Semi-colon on same line.
if not self.semicolon_newline:
# If whitespace is found then delete.
if whitespace_deletions:
fixes.extend(LintFix("delete", d) for d in whitespace_deletions)
# Semi-colon on new line.
else:
newline_deletions = [
segment
for segment in whitespace_deletions
if segment.is_type("newline")
]
non_newline_deletions = [
segment
for segment in whitespace_deletions
if not segment.is_type("newline")
]
# Remove pre-semi-colon whitespace.
fixes.extend(LintFix("delete", d) for d in non_newline_deletions)

if len(newline_deletions) == 0:
# Create missing newline.
fixes.append(LintFix("create", context.segment, NewlineSegment()))
if len(newline_deletions) > 1:
# Remove excess newlines.
fixes.extend(LintFix("delete", d) for d in newline_deletions[1:])

if fixes:
return LintResult(
anchor=anchor_segment,
fixes=fixes,
)

# SQL does not require a final trailing semi-colon, however
# this rule looks to enforce that it is there.
# Therefore we first locate the end of the file.
if self.require_final_semicolon:
if len(self.filter_meta(context.siblings_post)) > 0:
# This can only fail on the last segment
return None
elif len(context.segment.segments) > 0:
# This can only fail on the last base segment
return None
elif context.segment.is_meta:
# We can't fail on a meta segment
return None
else:
# So this looks like the end of the file, but we
# need to check that each parent segment is also the last.
# We do this with reference to the templated file, because it's
# the best we can do given the information available.
file_len = len(context.segment.pos_marker.templated_file.templated_str)
pos = context.segment.pos_marker.templated_slice.stop
# Does the length of the file equal the end of the templated position?
if file_len != pos:
return None

# Include current segment for complete stack.
complete_stack: List[BaseSegment] = list(context.raw_stack)
complete_stack.append(context.segment)

# Iterate backwards over complete stack to find
# if the final semi-colon is already present.
anchor_segment = context.segment
semi_colon_exist_flag = False
for segment in complete_stack[::-1]: # type: ignore
if segment.name == "semicolon":
semi_colon_exist_flag = True
elif (segment.name not in whitespace_set) and (not segment.is_meta):
break
anchor_segment = segment

if not semi_colon_exist_flag:
# Create the final semi-colon if it does not yet exist.
if not self.semicolon_newline:
fixes = [
LintFix(
"edit",
anchor_segment,
[
anchor_segment,
SymbolSegment(raw=";", type="symbol", name="semicolon"),
],
)
]
else:
fixes = [
LintFix(
"edit",
anchor_segment,
[
anchor_segment,
NewlineSegment(),
SymbolSegment(raw=";", type="symbol", name="semicolon"),
],
)
]

return LintResult(
anchor=anchor_segment,
fixes=fixes,
)

return None
139 changes: 139 additions & 0 deletions test/fixtures/rules/std_rule_cases/L052.yml
@@ -0,0 +1,139 @@
rule: L052

test_pass_semi_colon_same_line_default:
pass_str: |
SELECT a FROM foo;

test_fail_semi_colon_same_line_custom_newline:
fail_str: |
SELECT a FROM foo;
fix_str: |
SELECT a FROM foo
;
configs:
rules:
L052:
semicolon_newline: True

test_pass_no_semi_colon_default:
pass_str: |
SELECT a FROM foo

test_fail_no_semi_colon_custom_require:
fail_str: |
SELECT a FROM foo
fix_str: |
SELECT a FROM foo;
configs:
rules:
L052:
require_final_semicolon: True

test_fail_no_semi_colon_custom_require_newline:
fail_str: |
SELECT a FROM foo
fix_str: |
SELECT a FROM foo
;
configs:
rules:
L052:
require_final_semicolon: True
semicolon_newline: True

test_pass_multi_statement_semi_colon_default:
pass_str: |
SELECT a FROM foo;
SELECT b FROM bar;

test_fail_multi_statement_semi_colon_custom_newline:
fail_str: |
SELECT a FROM foo;
SELECT b FROM bar;
fix_str: |
SELECT a FROM foo
;
SELECT b FROM bar
;
configs:
rules:
L052:
semicolon_newline: True

test_pass_multi_statement_no_trailing_semi_colon_default:
pass_str: |
SELECT a FROM foo;
SELECT b FROM bar

test_pass_multi_statement_no_trailing_semi_colon_custom_require:
fail_str: |
SELECT a FROM foo;
SELECT b FROM bar
fix_str: |
SELECT a FROM foo;
SELECT b FROM bar;
configs:
rules:
L052:
require_final_semicolon: True

test_fail_multi_statement_no_trailing_semi_colon_custom_require_newline:
fail_str: |
SELECT a FROM foo;
SELECT b FROM bar
fix_str: |
SELECT a FROM foo
;
SELECT b FROM bar
configs:
rules:
L052:
semicolon_newline: True

test_fail_space_semi_colon_default:
fail_str: |
SELECT a FROM foo ;
fix_str: |
SELECT a FROM foo;

test_fail_newline_semi_colon_default:
fail_str: |
SELECT a FROM foo
;
fix_str: |
SELECT a FROM foo;

test_pass_newline_semi_colon_custom_newline:
pass_str: |
SELECT a FROM foo
;
configs:
rules:
L052:
semicolon_newline: True

test_fail_multi_statement_semi_colon_default:
fail_str: |
SELECT a FROM foo

;
SELECT b FROM bar ;
fix_str: |
SELECT a FROM foo;
SELECT b FROM bar;

test_fail_multiple_newlines_semi_colon_custom_require_newline:
fail_str: |
SELECT a
FROM foo

;
fix_str: |
SELECT a
FROM foo
;
configs:
rules:
L052:
require_final_semicolon: True
semicolon_newline: True