Example #1
0
    def _eval(self, segment, raw_stack, **kwargs):
        """Commas should be followed by a single whitespace unless followed by a comment.

        This is a slightly odd one, because we'll almost always evaluate from a point a few places
        after the problem site. NB: We need at least two segments behind us for this to work.
        """
        if len(raw_stack) < 2:
            return None

        cm1 = raw_stack[-1]
        cm2 = raw_stack[-2]
        if cm2.name == "comma":
            # comma followed by something that isn't whitespace?
            if cm1.name not in ["whitespace", "newline"]:
                ins = WhitespaceSegment(raw=" ")
                return LintResult(anchor=cm1,
                                  fixes=[LintFix("create", cm1, ins)])
            # comma followed by too much whitespace?
            if (cm1.raw != " "
                    and cm1.name != "newline") and not segment.is_comment:
                repl = WhitespaceSegment(raw=" ")
                return LintResult(anchor=cm1,
                                  fixes=[LintFix("edit", cm1, repl)])
        # Otherwise we're fine
        return None
Example #2
0
    def _eval(self, segment, parent_stack, **kwargs):
        """Trailing commas within select clause."""
        if segment.is_type("select_clause"):
            # Iterate content to find last element
            last_content = None
            for seg in segment.segments:
                if seg.is_code:
                    last_content = seg

            # What mode are we in?
            if self.select_clause_trailing_comma == "forbid":
                # Is it a comma?
                if last_content.is_type("comma"):
                    return LintResult(
                        anchor=last_content,
                        fixes=[LintFix("delete", last_content)],
                        description=
                        "Trailing comma in select statement forbidden",
                    )
            elif self.select_clause_trailing_comma == "require":
                if not last_content.is_type("comma"):
                    new_comma = self.make_symbol(",", seg_type="comma")
                    return LintResult(
                        anchor=last_content,
                        fixes=[
                            LintFix("edit", last_content,
                                    [last_content, new_comma])
                        ],
                        description=
                        "Trailing comma in select statement required",
                    )
        return None
Example #3
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        # We only care about commas.
        if context.segment.name != "comma":
            return None

        # Get subsequent whitespace segment and the first non-whitespace segment.
        subsequent_whitespace, first_non_whitespace = self._get_subsequent_whitespace(
            context
        )

        if (
            not subsequent_whitespace
            and (first_non_whitespace is not None)
            and (not first_non_whitespace.is_type("newline"))
        ):
            # No trailing whitespace and not followed by a newline,
            # therefore create a whitespace after the comma.
            return LintResult(
                anchor=first_non_whitespace,
                fixes=[LintFix.create_after(context.segment, [WhitespaceSegment()])],
            )
        elif (
            subsequent_whitespace
            and (subsequent_whitespace.raw != " ")
            and (first_non_whitespace is not None)
            and (not first_non_whitespace.is_comment)
        ):
            # Excess trailing whitespace therefore edit to only be one space long.
            return LintResult(
                anchor=subsequent_whitespace,
                fixes=[LintFix.replace(subsequent_whitespace, [WhitespaceSegment()])],
            )

        return None
Example #4
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Find rule violations and provide fixes.

        0. Look for a case expression
        1. Look for "ELSE"
        2. Mark "ELSE" for deletion (populate "fixes")
        3. Backtrack and mark all newlines/whitespaces for deletion
        4. Look for a raw "NULL" segment
        5.a. The raw "NULL" segment is found, we mark it for deletion and return
        5.b. We reach the end of case when without matching "NULL": the rule passes
        """
        if context.segment.is_type("case_expression"):
            children = context.functional.segment.children()
            else_clause = children.first(sp.is_type("else_clause"))

            # Does the "ELSE" have a "NULL"? NOTE: Here, it's safe to look for
            # "NULL", as an expression would *contain* NULL but not be == NULL.
            if else_clause and else_clause.children(
                    lambda child: child.raw_upper == "NULL"):
                # Found ELSE with NULL. Delete the whole else clause as well as
                # indents/whitespaces/meta preceding the ELSE. :TRICKY: Note
                # the use of reversed() to make select() effectively search in
                # reverse.
                before_else = children.reversed().select(
                    start_seg=else_clause[0],
                    loop_while=sp.or_(sp.is_name("whitespace", "newline"),
                                      sp.is_meta()),
                )
                return LintResult(
                    anchor=context.segment,
                    fixes=[LintFix.delete(else_clause[0])] +
                    [LintFix.delete(seg) for seg in before_else],
                )
        return None
Example #5
0
 def _coerce_indent_to(self, desired_indent, current_indent_buffer,
                       current_anchor):
     """Generate fixes to make an indent a certain size."""
     # If there shouldn't be an indent at all, just delete.
     if len(desired_indent) == 0:
         fixes = [LintFix("delete", elem) for elem in current_indent_buffer]
     # If we don't have any indent and we should, then add a single
     elif len("".join(elem.raw for elem in current_indent_buffer)) == 0:
         fixes = [
             LintFix(
                 "create",
                 current_anchor,
                 self.make_whitespace(raw=desired_indent,
                                      pos_marker=current_anchor.pos_marker),
             )
         ]
     # Otherwise edit the first element to be the right size
     else:
         # Edit the first element of this line's indent.
         fixes = [
             LintFix(
                 "edit",
                 current_indent_buffer[0],
                 self.make_whitespace(
                     raw=desired_indent,
                     pos_marker=current_indent_buffer[0].pos_marker,
                 ),
             )
         ]
     return fixes
Example #6
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Trailing commas within select clause."""
        # Config type hints
        self.select_clause_trailing_comma: str

        segment = context.functional.segment
        children = segment.children()
        if segment.all(sp.is_type("select_clause")):
            # Iterate content to find last element
            last_content: BaseSegment = children.last(sp.is_code())[0]

            # What mode are we in?
            if self.select_clause_trailing_comma == "forbid":
                # Is it a comma?
                if last_content.is_type("comma"):
                    return LintResult(
                        anchor=last_content,
                        fixes=[LintFix.delete(last_content)],
                        description=
                        "Trailing comma in select statement forbidden",
                    )
            elif self.select_clause_trailing_comma == "require":
                if not last_content.is_type("comma"):
                    new_comma = SymbolSegment(",", name="comma", type="comma")
                    return LintResult(
                        anchor=last_content,
                        fixes=[
                            LintFix.replace(last_content,
                                            [last_content, new_comma])
                        ],
                        description=
                        "Trailing comma in select statement required",
                    )
        return None
Example #7
0
    def _eval(self, segment, **kwargs):
        """Find rule violations and provide fixes.

        0. Look for a case expression
        1. Look for "ELSE"
        2. Mark "ELSE" for deletion (populate "fixes")
        3. Backtrack and mark all newlines/whitespaces for deletion
        4. Look for a raw "NULL" segment
        5.a. The raw "NULL" segment is found, we mark it for deletion and return
        5.b. We reach the end of case when without matching "NULL": the rule passes
        """
        if segment.is_type("case_expression"):
            fixes = []
            for idx, seg in enumerate(segment.segments):
                # When we find ELSE we delete
                # everything up to NULL
                if fixes:
                    fixes.append(LintFix("delete", seg))
                    # Safe to look for NULL, as an expression
                    # would contain NULL but not be == NULL
                    if seg.raw_upper == "NULL":
                        return LintResult(anchor=segment, fixes=fixes)

                if not fixes and seg.name == "ELSE":
                    fixes.append(LintFix("delete", seg))
                    # Walk back to remove indents/whitespaces
                    walk_idx = idx - 1
                    while (
                        segment.segments[walk_idx].name == "whitespace"
                        or segment.segments[walk_idx].name == "newline"
                        or segment.segments[walk_idx].is_meta
                    ):
                        fixes.append(LintFix("delete", segment.segments[walk_idx]))
                        walk_idx = walk_idx - 1
Example #8
0
    def _eval_multiple_select_target_elements(self, select_targets_info,
                                              segment):
        """Multiple select targets. Ensure each is on a separate line."""
        # Insert newline before every select target.
        fixes = []
        for i, select_target in enumerate(select_targets_info.select_targets):
            base_segment = (segment if not i else
                            select_targets_info.select_targets[i - 1])
            if (base_segment.pos_marker.working_line_no ==
                    select_target.pos_marker.working_line_no):
                # Find and delete any whitespace before the select target.
                start_seg = select_targets_info.select_idx
                # If any select modifier (e.g. distinct ) is present, start
                # there rather than at the beginning.
                modifier = segment.get_child("select_clause_modifier")
                if modifier:
                    start_seg = segment.segments.index(modifier)

                ws_to_delete = segment.select_children(
                    start_seg=segment.segments[start_seg]
                    if not i else select_targets_info.select_targets[i - 1],
                    select_if=lambda s: s.is_type("whitespace"),
                    loop_while=lambda s: s.is_type("whitespace", "comma") or s.
                    is_meta,
                )
                fixes += [LintFix("delete", ws) for ws in ws_to_delete]
                fixes.append(LintFix("create", select_target,
                                     NewlineSegment()))
        if fixes:
            return LintResult(anchor=segment, fixes=fixes)
Example #9
0
    def _eval(self, segment, parent_stack, raw_stack, **kwargs):
        """Implicit aliasing of table/column not allowed. Use explicit `AS` clause.

        We look for the alias segment, and then evaluate its parent and whether
        it contains an AS keyword. This is the _eval function for both L011 and L012.

        The use of `raw_stack` is just for working out how much whitespace to add.

        """
        fixes = []

        if segment.is_type("alias_expression"):
            if parent_stack[-1].is_type(*self._target_elems):
                if any(e.name.lower() == "as" for e in segment.segments):
                    if self.aliasing == "implicit":
                        if segment.segments[0].name.lower() == "as":

                            # Remove the AS as we're using implict aliasing
                            fixes.append(LintFix("delete",
                                                 segment.segments[0]))
                            anchor = raw_stack[-1]

                            # Remove whitespace before (if exists) or after (if not)
                            if (len(raw_stack) > 0
                                    and raw_stack[-1].type == "whitespace"):
                                fixes.append(LintFix("delete", raw_stack[-1]))
                            elif (len(segment.segments) > 0 and
                                  segment.segments[1].type == "whitespace"):
                                fixes.append(
                                    LintFix("delete", segment.segments[1]))

                            return LintResult(anchor=anchor, fixes=fixes)

                else:
                    insert_buff = []

                    # Add initial whitespace if we need to...
                    if raw_stack[-1].name not in ["whitespace", "newline"]:
                        insert_buff.append(WhitespaceSegment())

                    # Add an AS (Uppercase for now, but could be corrected later)
                    insert_buff.append(KeywordSegment("AS"))

                    # Add a trailing whitespace if we need to
                    if segment.segments[0].name not in [
                            "whitespace", "newline"
                    ]:
                        insert_buff.append(WhitespaceSegment())

                    return LintResult(
                        anchor=segment,
                        fixes=[
                            LintFix("create", segment.segments[0], insert_buff)
                        ],
                    )
        return None
Example #10
0
    def _eval(self, segment, raw_stack, **kwargs):
        """Looking for DISTINCT before a bracket.

        Look for DISTINCT keyword immediately followed by open parenthesis.
        """
        # We only trigger when "DISTINCT" is the immediate parent of an
        # expression that begins with start_bracket.
        raw_stack_filtered = self.filter_meta(raw_stack)
        if raw_stack_filtered and raw_stack_filtered[-1].name == "DISTINCT":
            if segment.type == "expression":
                segments_filtered = self.filter_meta(segment.segments)
                if segments_filtered and segments_filtered[0].type == "start_bracket":
                    # If we find open_bracket immediately following DISTINCT,
                    # then bad.
                    fixes = []
                    # The end bracket could be anywhere in segments_filtered,
                    # e.g. if the expression is (a + b) * c. If and only if it's
                    # at the *end*, then the parentheses are unnecessary and
                    # confusing. Remove them.
                    if segments_filtered[-1].type == "end_bracket":
                        fixes += [
                            LintFix("delete", segments_filtered[0]),
                            LintFix("delete", segments_filtered[-1]),
                        ]
                        # Update segments_filtered to reflect the pending
                        # deletions.
                        segments_filtered = segments_filtered[1:-1]
                    # If there are still segments remaining after the potential
                    # deletions above, insert a space between DISTINCT and the
                    # remainder of the expression. (I think there will always
                    # be remaining segments; this is a sanity check to ensure
                    # we don't cause an IndexError.)
                    if segments_filtered:
                        # Insert a single space after the open parenthesis being
                        # removed. Reason: DISTINCT is not a function; it's more
                        # of a modifier that acts on all the columns. Therefore,
                        # adding a space makes it clearer what the SQL is
                        # actually doing.
                        insert_str = " "
                        first_segment = segments_filtered[0]
                        fixes.append(
                            LintFix(
                                "create",
                                first_segment,
                                [
                                    self.make_whitespace(
                                        raw=insert_str,
                                        pos_marker=first_segment.pos_marker.advance_by(
                                            insert_str
                                        ),
                                    )
                                ],
                            )
                        )
                    return LintResult(anchor=segment, fixes=fixes)
        return None
Example #11
0
    def _eval(self, context: RuleContext) -> LintResult:
        """Look for UNION keyword not immediately followed by DISTINCT or ALL.

        Note that UNION DISTINCT is valid, rule only applies to bare UNION.
        The function does this by looking for a segment of type set_operator
        which has a UNION but no DISTINCT or ALL.

        Note only some dialects have concept of UNION DISTINCT, so rule is only
        applied to dialects that are known to support this syntax.
        """
        if context.dialect.name not in [
                "ansi",
                "bigquery",
                "hive",
                "mysql",
                "redshift",
        ]:
            return LintResult()

        if context.segment.is_type("set_operator"):
            if "union" in context.segment.raw and not (
                    "ALL" in context.segment.raw.upper()
                    or "DISTINCT" in context.segment.raw.upper()):
                return LintResult(
                    anchor=context.segment,
                    fixes=[
                        LintFix.replace(
                            context.segment.segments[0],
                            [
                                KeywordSegment("union"),
                                WhitespaceSegment(),
                                KeywordSegment("distinct"),
                            ],
                        )
                    ],
                )
            elif "UNION" in context.segment.raw.upper() and not (
                    "ALL" in context.segment.raw.upper()
                    or "DISTINCT" in context.segment.raw.upper()):
                return LintResult(
                    anchor=context.segment,
                    fixes=[
                        LintFix.replace(
                            context.segment.segments[0],
                            [
                                KeywordSegment("UNION"),
                                WhitespaceSegment(),
                                KeywordSegment("DISTINCT"),
                            ],
                        )
                    ],
                )
        return LintResult()
Example #12
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Files must end with a single trailing newline.

        We only care about the segment and the siblings which come after it
        for this rule, we discard the others into the kwargs argument.

        """
        # We only care about the final segment of the parse tree.
        if not self.is_final_segment(context):
            return None

        # Include current segment for complete stack and reverse.
        parent_stack: Segments = context.functional.parent_stack
        complete_stack: Segments = (context.functional.raw_stack +
                                    context.functional.segment)
        reversed_complete_stack = complete_stack.reversed()

        # Find the trailing newline segments.
        trailing_newlines = reversed_complete_stack.select(
            select_if=sp.is_type("newline"),
            loop_while=sp.or_(sp.is_whitespace(), sp.is_type("dedent")),
        )
        trailing_literal_newlines = trailing_newlines.select(
            loop_while=lambda seg: sp.templated_slices(
                seg, context.templated_file).all(tsp.is_slice_type("literal")))

        if not trailing_literal_newlines:
            # We make an edit to create this segment after the child of the FileSegment.
            if len(parent_stack) == 1:
                fix_anchor_segment = context.segment
            else:
                fix_anchor_segment = parent_stack[1]

            return LintResult(
                anchor=context.segment,
                fixes=[
                    LintFix.create_after(
                        fix_anchor_segment,
                        [NewlineSegment()],
                    )
                ],
            )
        elif len(trailing_literal_newlines) > 1:
            # Delete extra newlines.
            return LintResult(
                anchor=context.segment,
                fixes=[
                    LintFix.delete(d) for d in trailing_literal_newlines[1:]
                ],
            )
        else:
            # Single newline, no need for fix.
            return None
Example #13
0
    def _handle_semicolon_newline(
            self, context: RuleContext,
            info: SegmentMoveContext) -> Optional[LintResult]:
        # Adjust before_segment and anchor_segment for preceding inline
        # comments. Inline comments can contain noqa logic so we need to add the
        # newline after the inline comment.
        (
            before_segment,
            anchor_segment,
        ) = self._handle_preceding_inline_comments(info.before_segment,
                                                   info.anchor_segment)

        if (len(before_segment) == 1) and all(
                s.is_type("newline") for s in before_segment):
            return None

        # If preceding segment is not a single newline then delete the old
        # semi-colon/preceding whitespace and then insert the
        # semi-colon in the correct location.

        # This handles an edge case in which an inline comment comes after
        # the semi-colon.
        anchor_segment = self._handle_trailing_inline_comments(
            context, anchor_segment)
        fixes = []
        if anchor_segment is context.segment:
            fixes.append(
                LintFix.replace(
                    anchor_segment,
                    [
                        NewlineSegment(),
                        SymbolSegment(raw=";", type="symbol",
                                      name="semicolon"),
                    ],
                ))
        else:
            fixes.extend([
                LintFix.replace(
                    anchor_segment,
                    [
                        anchor_segment,
                        NewlineSegment(),
                        SymbolSegment(raw=";", type="symbol",
                                      name="semicolon"),
                    ],
                ),
                LintFix.delete(context.segment, ),
            ])
            fixes.extend(LintFix.delete(d) for d in info.whitespace_deletions)
        return LintResult(
            anchor=anchor_segment,
            fixes=fixes,
        )
Example #14
0
    def _eval(self, segment, **kwargs):
        """Find rule violations and provide fixes."""
        if (
            segment.is_type("function")
            and segment.get_child("function_name").raw_upper == "COUNT"
        ):
            f_content = [
                seg
                for seg in segment.segments
                if not seg.is_meta
                and not seg.is_type(
                    "start_bracket",
                    "end_bracket",
                    "function_name",
                    "whitespace",
                    "newline",
                )
            ]
            if len(f_content) != 1:
                return None

            if self.prefer_count_1 and f_content[0].type == "star":
                return LintResult(
                    anchor=segment,
                    fixes=[
                        LintFix(
                            "edit",
                            f_content[0],
                            f_content[0].edit(f_content[0].raw.replace("*", "1")),
                        ),
                    ],
                )
            if not self.prefer_count_1 and f_content[0].type == "expression":
                expression_content = [
                    seg for seg in f_content[0].segments if not seg.is_meta
                ]
                if (
                    len(expression_content) == 1
                    and expression_content[0].type == "literal"
                    and expression_content[0].raw == "1"
                ):
                    return LintResult(
                        anchor=segment,
                        fixes=[
                            LintFix(
                                "edit",
                                expression_content[0],
                                expression_content[0].edit(
                                    expression_content[0].raw.replace("1", "*")
                                ),
                            ),
                        ],
                    )
Example #15
0
    def _lint_references_and_aliases(
        self,
        table_aliases,
        value_table_function_aliases,
        references,
        col_aliases,
        using_cols,
        parent_select,
    ):
        """Check all aliased references against tables referenced in the query."""
        # A buffer to keep any violations.
        violation_buff = []
        # Check all the references that we have, keep track of which aliases we refer to.
        tbl_refs = set()
        for r in references:
            tbl_refs.update(
                tr.part
                for tr in r.extract_possible_references(
                    level=r.ObjectReferenceLevel.TABLE
                )
            )

        alias: AliasInfo
        for alias in table_aliases:
            if alias.aliased and alias.ref_str not in tbl_refs:
                fixes = [LintFix("delete", alias.alias_expression)]
                found_alias_segment = False
                # Walk back to remove indents/whitespaces
                for segment in reversed(alias.from_expression_element.segments):
                    if not found_alias_segment:
                        if segment is alias.alias_expression:
                            found_alias_segment = True
                    else:
                        if (
                            segment.name == "whitespace"
                            or segment.name == "newline"
                            or segment.is_meta
                        ):
                            fixes.append(LintFix("delete", segment))
                        else:
                            # Stop once we reach an other, "regular" segment.
                            break
                violation_buff.append(
                    LintResult(
                        anchor=alias.segment,
                        description="Alias {!r} is never used in SELECT statement.".format(
                            alias.ref_str
                        ),
                        fixes=fixes,
                    )
                )
        return violation_buff or None
Example #16
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Fully qualify JOINs."""
        # Config type hints
        self.fully_qualify_join_types: str

        # We are only interested in JOIN clauses.
        if not context.segment.is_type("join_clause"):
            return None

        join_clause_keywords = [
            segment for segment in context.segment.segments if segment.type == "keyword"
        ]

        # Identify LEFT/RIGHT/OUTER JOIN and if the next keyword is JOIN.
        if (
            self.fully_qualify_join_types in ["outer", "both"]
            and join_clause_keywords[0].raw_upper in ["RIGHT", "LEFT", "FULL"]
            and join_clause_keywords[1].raw_upper == "JOIN"
        ):
            # Define basic-level OUTER capitalization based on JOIN
            outer_kw = ("outer", "OUTER")[join_clause_keywords[1].raw == "JOIN"]
            # Insert OUTER after LEFT/RIGHT/FULL
            return LintResult(
                context.segment.segments[0],
                fixes=[
                    LintFix.create_after(
                        context.segment.segments[0],
                        [WhitespaceSegment(), KeywordSegment(outer_kw)],
                    )
                ],
            )

        # Identify lone JOIN by looking at first child segment.
        if (
            self.fully_qualify_join_types in ["inner", "both"]
            and join_clause_keywords[0].raw_upper == "JOIN"
        ):
            # Define basic-level INNER capitalization based on JOIN
            inner_kw = ("inner", "INNER")[join_clause_keywords[0].raw == "JOIN"]
            # Replace lone JOIN with INNER JOIN.
            return LintResult(
                context.segment.segments[0],
                fixes=[
                    LintFix.create_before(
                        context.segment.segments[0],
                        [KeywordSegment(inner_kw), WhitespaceSegment()],
                    )
                ],
            )

        return None
Example #17
0
 def _eval(self, context: RuleContext) -> Optional[List[LintResult]]:
     """Single whitespace expected in mother middle segment."""
     error_buffer: List[LintResult] = []
     if context.segment.is_type(self.expected_mother_segment_type):
         last_code = None
         mid_segs: List[BaseSegment] = []
         for seg in context.segment.iter_segments(
                 expanding=self.expand_children):
             if seg.is_code:
                 if (last_code and self.matches_target_tuples(
                         last_code, [self.pre_segment_identifier])
                         and self.matches_target_tuples(
                             seg, [self.post_segment_identifier])):
                     # Do we actually have the right amount of whitespace?
                     raw_inner = "".join(s.raw for s in mid_segs)
                     if raw_inner != " " and not (self.allow_newline and
                                                  any(s.name == "newline"
                                                      for s in mid_segs)):
                         if not raw_inner.strip():
                             # There's some whitespace and/or newlines, or nothing
                             fixes = []
                             if raw_inner:
                                 # There's whitespace and/or newlines. Drop those.
                                 fixes += [
                                     LintFix.delete(mid_seg)
                                     for mid_seg in mid_segs
                                 ]
                             # Enforce a single space
                             fixes += [
                                 LintFix.create_before(
                                     seg,
                                     [WhitespaceSegment()],
                                 )
                             ]
                         else:
                             # Don't otherwise suggest a fix for now.
                             # Only whitespace & newlines are covered.
                             # At least a comment section between `AS` and `(` can
                             # result in an unfixable error.
                             # TODO: Enable more complex fixing here.
                             fixes = None  # pragma: no cover
                         error_buffer.append(
                             LintResult(anchor=last_code, fixes=fixes))
                 mid_segs = []
                 if not seg.is_meta:
                     last_code = seg
             else:
                 mid_segs.append(seg)
     return error_buffer or None
Example #18
0
    def _eval_multiple_select_target_elements(self, select_targets_info,
                                              segment):
        """Multiple select targets. Ensure each is on a separate line."""
        # Insert newline before every select target.
        fixes = []
        for i, select_target in enumerate(select_targets_info.select_targets):
            base_segment = (segment if not i else
                            select_targets_info.select_targets[i - 1])
            if (base_segment.pos_marker.working_line_no ==
                    select_target.pos_marker.working_line_no):
                # Find and delete any whitespace before the select target.
                start_seg = select_targets_info.select_idx
                # If any select modifier (e.g. distinct ) is present, start
                # there rather than at the beginning.
                modifier = segment.get_child("select_clause_modifier")
                if modifier:
                    start_seg = segment.segments.index(modifier)

                ws_to_delete = segment.select_children(
                    start_seg=segment.segments[start_seg]
                    if not i else select_targets_info.select_targets[i - 1],
                    select_if=lambda s: s.is_type("whitespace"),
                    loop_while=lambda s: s.is_type("whitespace", "comma") or s.
                    is_meta,
                )
                fixes += [LintFix.delete(ws) for ws in ws_to_delete]
                fixes.append(
                    LintFix.create_before(select_target, [NewlineSegment()]))

            # If we are at the last select target check if the FROM clause
            # is on the same line, and if so move it to its own line.
            if select_targets_info.from_segment:
                if (i + 1 == len(select_targets_info.select_targets)) and (
                        select_target.pos_marker.working_line_no
                        == select_targets_info.from_segment.pos_marker.
                        working_line_no):
                    fixes.extend([
                        LintFix.delete(ws)
                        for ws in select_targets_info.pre_from_whitespace
                    ])
                    fixes.append(
                        LintFix.create_before(
                            select_targets_info.from_segment,
                            [NewlineSegment()],
                        ))

        if fixes:
            return LintResult(anchor=segment, fixes=fixes)
Example #19
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Use ``COALESCE`` instead of ``IFNULL`` or ``NVL``."""
        # We only care about function names.
        if context.segment.name != "function_name_identifier":
            return None

        # Only care if the function is ``IFNULL`` or ``NVL``.
        if context.segment.raw_upper not in {"IFNULL", "NVL"}:
            return None

        # Create fix to replace ``IFNULL`` or ``NVL`` with ``COALESCE``.
        fix = LintFix.replace(
            context.segment,
            [
                CodeSegment(
                    raw="COALESCE",
                    name="function_name_identifier",
                    type="function_name_identifier",
                )
            ],
        )

        return LintResult(
            anchor=context.segment,
            fixes=[fix],
            description=
            f"Use 'COALESCE' instead of '{context.segment.raw_upper}'.",
        )
Example #20
0
def _mock_crawl(rule,
                segment,
                ignore_mask,
                templated_file=None,
                *args,
                **kwargs):
    # For test__cli__fix_loop_limit_behavior, we mock BaseRule.crawl(),
    # replacing it with this function. This function generates an infinite
    # sequence of fixes without ever repeating the same fix. This causes the
    # linter to hit the loop limit, allowing us to test that behavior.
    if segment.is_type("comment") and "Comment" in segment.raw:
        global _fix_counter
        _fix_counter += 1
        fix = LintFix.replace(segment,
                              [CommentSegment(f"-- Comment {_fix_counter}")])
        result = LintResult(segment, fixes=[fix])
        errors = []
        fixes = []
        rule._process_lint_result(result, templated_file, ignore_mask, errors,
                                  fixes)
        return (
            errors,
            None,
            fixes,
            None,
        )
    else:
        return _old_crawl(rule,
                          segment,
                          ignore_mask,
                          templated_file=templated_file,
                          *args,
                          **kwargs)
Example #21
0
    def _get_fix(self, segment, fixed_raw):
        """Given a segment found to have a fix, returns a LintFix for it.

        May be overridden by subclasses, which is useful when the parse tree
        structure varies from this simple base case.
        """
        return LintFix("edit", segment, segment.edit(fixed_raw))
Example #22
0
    def _eval(self, segment, parent_stack, raw_stack, **kwargs):
        """Implicit aliasing of table/column not allowed. Use explicit `AS` clause.

        We look for the alias segment, and then evaluate its parent and whether
        it contains an AS keyword. This is the _eval function for both L011 and L012.

        The use of `raw_stack` is just for working out how much whitespace to add.

        """
        if segment.is_type("alias_expression"):
            if parent_stack[-1].is_type(*self._target_elems):
                if not any(e.name.lower() == "as" for e in segment.segments):
                    insert_buff = []
                    insert_str = ""

                    # Add initial whitespace if we need to...
                    if raw_stack[-1].name not in ["whitespace", "newline"]:
                        insert_buff.append(WhitespaceSegment())
                        insert_str += " "

                    # Add an AS (Uppercase for now, but could be corrected later)
                    insert_buff.append(KeywordSegment("AS"))
                    insert_str += "AS"

                    # Add a trailing whitespace if we need to
                    if segment.segments[0].name not in ["whitespace", "newline"]:
                        insert_buff.append(WhitespaceSegment())
                        insert_str += " "

                    return LintResult(
                        anchor=segment,
                        fixes=[LintFix("create", segment.segments[0], insert_buff)],
                    )
        return None
Example #23
0
    def _eval(self, segment, raw_stack, **kwargs):
        """Look for UNION keyword not immediately followed by DISTINCT or ALL.

        Note that UNION DISTINCT is valid, rule only applies to bare UNION.
        The function does this by looking for a segment of type set_operator
        which has a UNION but no DISTINCT or ALL.
        """
        if segment.type == "set_operator":
            if "UNION" in segment.raw.upper() and not (
                "ALL" in segment.raw.upper() or "DISTINCT" in segment.raw.upper()
            ):
                union = self.make_keyword(
                    raw="UNION",
                    pos_marker=segment.pos_marker,
                )
                ins = self.make_whitespace(raw=" ", pos_marker=segment.pos_marker)
                distinct = self.make_keyword(
                    raw="DISTINCT",
                    pos_marker=segment.pos_marker,
                )
                return LintResult(
                    anchor=segment,
                    fixes=[
                        LintFix("edit", segment.segments[0], [union, ins, distinct])
                    ],
                )
        return LintResult()
Example #24
0
 def _eval(self, segment, raw_stack, **kwargs):
     """Stars make newlines."""
     if segment.is_type("star"):
         return LintResult(
             anchor=segment,
             fixes=[LintFix("create", segment, self.make_newline())],
         )
Example #25
0
    def _coalesce_fix_list(
        context: RuleContext,
        coalesce_arg_1: BaseSegment,
        coalesce_arg_2: BaseSegment,
        preceding_not: bool = False,
    ) -> List[LintFix]:
        """Generate list of fixes to convert CASE statement to COALESCE function."""
        # Add coalesce and opening parenthesis.
        edits = [
            KeywordSegment("coalesce"),
            SymbolSegment("(", name="start_bracket", type="start_bracket"),
            coalesce_arg_1,
            SymbolSegment(",", name="comma", type="comma"),
            WhitespaceSegment(),
            coalesce_arg_2,
            SymbolSegment(")", name="end_bracket", type="end_bracket"),
        ]

        if preceding_not:
            not_edits: List[BaseSegment] = [
                KeywordSegment("not"),
                WhitespaceSegment(),
            ]
            edits = not_edits + edits

        fixes = [LintFix.replace(
            context.segment,
            edits,
        )]
        return fixes
Example #26
0
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
        """Mixed Tabs and Spaces in single whitespace.

        Only trigger from whitespace segments if they contain
        multiple kinds of whitespace.
        """
        # Config type hints
        self.tab_space_size: int

        if context.segment.is_type("whitespace"):
            if " " in context.segment.raw and "\t" in context.segment.raw:
                if len(context.raw_stack
                       ) == 0 or context.raw_stack[-1].is_type("newline"):
                    # We've got a single whitespace at the beginning of a line.
                    # It's got a mix of spaces and tabs. Replace each tab with
                    # a multiple of spaces
                    return LintResult(
                        anchor=context.segment,
                        fixes=[
                            LintFix.replace(
                                context.segment,
                                [
                                    context.segment.edit(
                                        context.segment.raw.replace(
                                            "\t", " " * self.tab_space_size)),
                                ],
                            ),
                        ],
                    )
        return None
Example #27
0
    def _eval(self, context: RuleContext) -> LintResult:
        """Function name not immediately followed by bracket.

        Look for Function Segment with anything other than the
        function name before brackets
        """
        segment = context.functional.segment
        # We only trigger on start_bracket (open parenthesis)
        if segment.all(sp.is_type("function")):
            children = segment.children()

            function_name = children.first(sp.is_type("function_name"))[0]
            start_bracket = children.first(sp.is_type("bracketed"))[0]

            intermediate_segments = children.select(start_seg=function_name,
                                                    stop_seg=start_bracket)
            if intermediate_segments:
                # It's only safe to fix if there is only whitespace
                # or newlines in the intervening section.
                if intermediate_segments.all(
                        sp.is_type("whitespace", "newline")):
                    return LintResult(
                        anchor=intermediate_segments[0],
                        fixes=[
                            LintFix.delete(seg)
                            for seg in intermediate_segments
                        ],
                    )
                else:
                    # It's not all whitespace, just report the error.
                    return LintResult(anchor=intermediate_segments[0], )

        return LintResult()
Example #28
0
    def _eval(self, segment, siblings_post, parent_stack, **kwargs):
        """Files must end with a trailing newline.

        We only care about the segment and the siblings which come after it
        for this rule, we discard the others into the kwargs argument.

        """
        if len(self.filter_meta(siblings_post)) > 0:
            # This can only fail on the last segment
            return None
        elif len(segment.segments) > 0:
            # This can only fail on the last base segment
            return None
        elif segment.name == "newline":
            # If this is the last segment, and it's a newline then we're good
            return None
        elif 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
            file_len = len(parent_stack[0].raw)
            pos = segment.pos_marker.char_pos
            # Does the length of the file, equal the length of the segment plus its position
            if file_len != pos + len(segment.raw):
                return None

        ins = self.make_newline(
            pos_marker=segment.pos_marker.advance_by(segment.raw))
        # We're going to make an edit because otherwise we would never get a match!
        return LintResult(anchor=segment,
                          fixes=[LintFix("edit", segment, [segment, ins])])
Example #29
0
    def _eval(self, segment, raw_stack, **kwargs):
        """Look for UNION keyword not immediately followed by DISTINCT or ALL.

        Note that UNION DISTINCT is valid, rule only applies to bare UNION.
        The function does this by looking for a segment of type set_operator
        which has a UNION but no DISTINCT or ALL.
        """
        if segment.is_type("set_operator"):
            if "UNION" in segment.raw.upper() and not (
                "ALL" in segment.raw.upper() or "DISTINCT" in segment.raw.upper()
            ):
                return LintResult(
                    anchor=segment,
                    fixes=[
                        LintFix(
                            "edit",
                            segment.segments[0],
                            [
                                KeywordSegment("UNION"),
                                WhitespaceSegment(),
                                KeywordSegment("DISTINCT"),
                            ],
                        )
                    ],
                )
        return LintResult()
Example #30
0
    def _eval(self, segment, **kwargs):
        """Select clause modifiers must appear on same line as SELECT."""
        if segment.is_type("select_clause"):
            # Does the select clause have modifiers?
            select_modifier = segment.get_child("select_clause_modifier")
            if not select_modifier:
                return None  # No. We're done.
            select_modifier_idx = segment.segments.index(select_modifier)

            # Does the select clause contain a newline?
            newline = segment.get_child("newline")
            if not newline:
                return None  # No. We're done.
            newline_idx = segment.segments.index(newline)

            # Is there a newline before the select modifier?
            if newline_idx > select_modifier_idx:
                return None  # No, we're done.

            # Yes to all the above. We found an issue.

            # E.g.: " DISTINCT\n"
            replace_newline_with = [
                self.make_whitespace(raw=" "),
                select_modifier,
                self.make_newline(),
            ]
            fixes = [
                # E.g. "\n" -> " DISTINCT\n.
                LintFix("edit", newline, replace_newline_with),
                # E.g. "DISTINCT" -> X
                LintFix("delete", select_modifier),
            ]

            # E.g. " " after "DISTINCT"
            ws_to_delete = segment.select_children(
                start_seg=select_modifier,
                select_if=lambda s: s.is_type("whitespace"),
                loop_while=lambda s: s.is_type("whitespace") or s.is_meta,
            )

            # E.g. " " -> X
            fixes += [LintFix("delete", ws) for ws in ws_to_delete]
            return LintResult(
                anchor=segment,
                fixes=fixes,
            )