def _eval(self, context: RuleContext) -> Optional[LintResult]: """Use ``!=`` instead of ``<>`` for "not equal to" comparison.""" # Only care about not_equal_to segments. We should only get # comparison operator types from the crawler, but not all will # be "not_equal_to". if context.segment.name != "not_equal_to": return None # Get the comparison operator children raw_comparison_operators = ( FunctionalContext(context) .segment.children() .select(select_if=sp.is_type("raw_comparison_operator")) ) # Only care about ``<>`` if [r.raw for r in raw_comparison_operators] != ["<", ">"]: return None # Provide a fix and replace ``<>`` with ``!=`` # As each symbol is a separate symbol this is done in two steps: # 1. Replace < with ! # 2. Replace > with = fixes = [ LintFix.replace( raw_comparison_operators[0], [SymbolSegment(raw="!", type="raw_comparison_operator")], ), LintFix.replace( raw_comparison_operators[1], [SymbolSegment(raw="=", type="raw_comparison_operator")], ), ] return LintResult(context.segment, fixes)
def _eval(self, context: RuleContext) -> Optional[LintResult]: # We only care about commas. assert context.segment.is_type("comma") # 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
def _eval(self, context: RuleContext) -> Optional[LintResult]: """Trailing commas within select clause.""" # Config type hints self.select_clause_trailing_comma: str segment = FunctionalContext(context).segment children = segment.children() # 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(",", 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
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 """ assert context.segment.is_type("case_expression") children = FunctionalContext(context).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
def _create_semicolon_and_delete_whitespace( self, target_segment: BaseSegment, parent_segment: BaseSegment, anchor_segment: BaseSegment, whitespace_deletions: Segments, create_segments: List[BaseSegment], ) -> List[LintFix]: anchor_segment = self._choose_anchor_segment(parent_segment, "create_after", anchor_segment, filter_meta=True) lintfix_fn = LintFix.create_after # :TRICKY: Use IdentitySet rather than set() since # different segments may compare as equal. whitespace_deletion_set = IdentitySet(whitespace_deletions) if anchor_segment in whitespace_deletion_set: # Can't delete() and create_after() the same segment. Use replace() # instead. lintfix_fn = LintFix.replace whitespace_deletions = whitespace_deletions.select( lambda seg: seg is not anchor_segment) fixes = [ lintfix_fn( anchor_segment, create_segments, ), LintFix.delete(target_segment, ), ] fixes.extend(LintFix.delete(d) for d in whitespace_deletions) return fixes
def _eval(self, context: RuleContext) -> LintResult: """Nested CASE statement in ELSE clause could be flattened.""" segment = FunctionalContext(context).segment assert segment.select(sp.is_type("case_expression")) case1_children = segment.children() case1_last_when = case1_children.last(sp.is_type("when_clause")).get() case1_else_clause = case1_children.select(sp.is_type("else_clause")) case1_else_expressions = case1_else_clause.children( sp.is_type("expression")) expression_children = case1_else_expressions.children() case2 = expression_children.select(sp.is_type("case_expression")) # The len() checks below are for safety, to ensure the CASE inside # the ELSE is not part of a larger expression. In that case, it's # not safe to simplify in this way -- we'd be deleting other code. if (not case1_last_when or len(case1_else_expressions) > 1 or len(expression_children) > 1 or not case2): return LintResult() # We can assert that this exists because of the previous check. assert case1_last_when # We can also assert that we'll also have an else clause because # otherwise the case2 check above would fail. case1_else_clause_seg = case1_else_clause.get() assert case1_else_clause_seg # Delete stuff between the last "WHEN" clause and the "ELSE" clause. case1_to_delete = case1_children.select(start_seg=case1_last_when, stop_seg=case1_else_clause_seg) # Delete the nested "CASE" expression. fixes = case1_to_delete.apply(lambda seg: LintFix.delete(seg)) # Determine the indentation to use when we move the nested "WHEN" # and "ELSE" clauses, based on the indentation of case1_last_when. # If no whitespace segments found, use default indent. indent = (case1_children.select( stop_seg=case1_last_when).reversed().select( sp.is_type("whitespace"))) indent_str = "".join(seg.raw for seg in indent) if indent else self.indent # Move the nested "when" and "else" clauses after the last outer # "when". nested_clauses = case2.children( sp.is_type("when_clause", "else_clause")) create_after_last_when = nested_clauses.apply( lambda seg: [NewlineSegment(), WhitespaceSegment(indent_str), seg]) segments = [ item for sublist in create_after_last_when for item in sublist ] fixes.append( LintFix.create_after(case1_last_when, segments, source=segments)) # Delete the outer "else" clause. fixes.append(LintFix.delete(case1_else_clause_seg)) return LintResult(case2[0], fixes=fixes)
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", "hive", "mysql", "redshift", ]: return LintResult() assert 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()
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)
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. parent_stack, segment = get_last_segment(FunctionalContext(context).segment) self.logger.debug("Found last segment as: %s", segment) trailing_newlines = Segments(*get_trailing_newlines(context.segment)) trailing_literal_newlines = trailing_newlines self.logger.debug( "Untemplated trailing newlines: %s", trailing_literal_newlines ) if context.templated_file: trailing_literal_newlines = trailing_newlines.select( loop_while=lambda seg: sp.templated_slices( seg, context.templated_file ).all(tsp.is_slice_type("literal")) ) self.logger.debug("Templated trailing newlines: %s", trailing_literal_newlines) 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 = segment[0] else: fix_anchor_segment = parent_stack[1] self.logger.debug("Anchor on: %s", fix_anchor_segment) return LintResult( anchor=segment[0], fixes=[ LintFix.create_after( fix_anchor_segment, [NewlineSegment()], ) ], ) elif len(trailing_literal_newlines) > 1: # Delete extra newlines. return LintResult( anchor=segment[0], fixes=[LintFix.delete(d) for d in trailing_literal_newlines[1:]], ) else: # Single newline, no need for fix. return None
def _generate_fixes( whitespace_segment: Optional[BaseSegment], ) -> Optional[List[LintFix]]: if whitespace_segment: return [ LintFix.replace( anchor_segment=whitespace_segment, # NB: Currently we are just inserting a Newline here. This alone will # produce not properly indented SQL. We rely on L003 to deal with # indentation later. # As a future improvement we could maybe add WhitespaceSegment( ... ) # here directly. edit_segments=[NewlineSegment()], ) ] else: # We should rarely reach here as set operators are always surrounded by either # WhitespaceSegment or NewlineSegment. # However, in exceptional cases the WhitespaceSegment might be enclosed in the # surrounding segment hierachy and not accessible by the rule logic. # At the time of writing this is true for `tsql` as covered in the test # `test_fail_autofix_in_tsql_disabled`. If we encounter such case, we skip # fixing. return []
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.replace(segment, [segment.edit(fixed_raw)])
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("(", type="start_bracket"), coalesce_arg_1, SymbolSegment(",", type="comma"), WhitespaceSegment(), coalesce_arg_2, SymbolSegment(")", 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
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 = FunctionalContext(context).segment # We only trigger on start_bracket (open parenthesis) assert 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()
def _eval(self, context: RuleContext) -> Optional[LintResult]: """Use ``COALESCE`` instead of ``IFNULL`` or ``NVL``.""" # We only care about function names, and they should be the # only things we get assert context.segment.is_type("function_name_identifier") # 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", type="function_name_identifier", )], ) return LintResult( anchor=context.segment, fixes=[fix], description= f"Use 'COALESCE' instead of '{context.segment.raw_upper}'.", )
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 not context.raw_stack 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
def _eval(self, context: RuleContext) -> Optional[List[LintResult]]: """Single whitespace expected in mother middle segment.""" error_buffer: List[LintResult] = [] 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.is_type("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
def _fixes_for_move_after_select_clause( stop_seg: BaseSegment, delete_segments: Optional[Segments] = None, add_newline: bool = True, ) -> List[LintFix]: """Cleans up by moving leftover select_clause segments. Context: Some of the other fixes we make in _eval_single_select_target_element() leave leftover child segments that need to be moved to become *siblings* of the select_clause. """ start_seg = ( modifier[0] if modifier else select_children[select_targets_info.first_new_line_idx] ) move_after_select_clause = select_children.select( start_seg=start_seg, stop_seg=stop_seg, ) # :TRICKY: Below, we have a couple places where we # filter to guard against deleting the same segment # multiple times -- this is illegal. # :TRICKY: Use IdentitySet rather than set() since # different segments may compare as equal. all_deletes = IdentitySet( fix.anchor for fix in fixes if fix.edit_type == "delete" ) fixes_ = [] for seg in delete_segments or []: if seg not in all_deletes: fixes.append(LintFix.delete(seg)) all_deletes.add(seg) fixes_ += [ LintFix.delete(seg) for seg in move_after_select_clause if seg not in all_deletes ] fixes_.append( LintFix.create_after( select_clause[0], ([NewlineSegment()] if add_newline else []) + list(move_after_select_clause), ) ) return fixes_
def _eval(self, context: RuleContext) -> List[LintResult]: """Top-level statements should not be wrapped in brackets.""" # Because of the root_only_crawler, this can control its own # crawling behaviour. results = [] for parent, bracketed_segment in self._iter_bracketed_statements( context.segment): self.logger.debug("Evaluating %s in %s", bracketed_segment, parent) # Replace the bracketed segment with it's # children, excluding the bracket symbols. bracket_set = {"start_bracket", "end_bracket"} filtered_children = Segments(*[ segment for segment in bracketed_segment.segments if segment.get_type() not in bracket_set and not segment.is_meta ]) # Lift leading/trailing whitespace and inline comments to the # segment above. This avoids introducing a parse error (ANSI and other # dialects generally don't allow this at lower levels of the parse # tree). to_lift_predicate = sp.or_(sp.is_whitespace(), sp.is_name("inline_comment")) leading = filtered_children.select(loop_while=to_lift_predicate) self.logger.debug("Leading: %s", leading) trailing = (filtered_children.reversed().select( loop_while=to_lift_predicate).reversed()) self.logger.debug("Trailing: %s", trailing) lift_nodes = IdentitySet(leading + trailing) fixes = [] if lift_nodes: fixes.append(LintFix.create_before(parent, list(leading))) fixes.append(LintFix.create_after(parent, list(trailing))) fixes.extend( [LintFix.delete(segment) for segment in lift_nodes]) filtered_children = filtered_children[len(leading ):-len(trailing)] fixes.append( LintFix.replace( bracketed_segment, filtered_children, )) results.append(LintResult(anchor=bracketed_segment, fixes=fixes)) return results
def _report_unused_alias(cls, alias: AliasInfo) -> LintResult: fixes = [LintFix.delete(alias.alias_expression)] # type: ignore # Walk back to remove indents/whitespaces to_delete = ( Segments( *alias.from_expression_element.segments).reversed().select( start_seg=alias.alias_expression, # Stop once we reach an other, "regular" segment. loop_while=sp.or_(sp.is_whitespace(), sp.is_meta()), )) fixes += [LintFix.delete(seg) for seg in to_delete] return LintResult( anchor=alias.segment, description="Alias {!r} is never used in SELECT statement.".format( alias.ref_str), fixes=fixes, )
def _eval(self, context: RuleContext) -> LintResult: """Incorrect indentation found in file.""" # Config type hints self.tab_space_size: int self.indent_unit: str tab = "\t" space = " " correct_indent = self.indent wrong_indent = ( tab if self.indent_unit == "space" else space * self.tab_space_size ) if ( context.segment.is_type("whitespace") and wrong_indent in context.segment.raw ): fixes = [] description = "Incorrect indentation type found in file." edit_indent = context.segment.raw.replace(wrong_indent, correct_indent) pre_seg = context.raw_stack[-1] if context.raw_stack else None # Ensure that the number of space indents is a multiple of tab_space_size # before attempting to convert spaces to tabs to avoid mixed indents # unless we are converted tabs to spaces (indent_unit = space) if ( ( self.indent_unit == "space" or context.segment.raw.count(space) % self.tab_space_size == 0 ) # Only attempt a fix at the start of a newline for now and (pre_seg is None or pre_seg.is_type("newline")) ): fixes = [ LintFix.replace( context.segment, [ WhitespaceSegment(raw=edit_indent), ], ) ] elif not (pre_seg is None or pre_seg.is_type("newline")): # give a helpful message if the wrong indent has been found and is not # at the start of a newline description += ( " The indent occurs after other text, so a manual fix is needed." ) else: # If we get here, the indent_unit is tabs, and the number of spaces is # not a multiple of tab_space_size description += " The number of spaces is not a multiple of " "tab_space_size, so a manual fix is needed." return LintResult( anchor=context.segment, fixes=fixes, description=description ) return LintResult()
def _eval(self, context): """Stars make newlines.""" if context.segment.is_type("whitespace"): return LintResult( anchor=context.segment, fixes=[ LintFix.replace( context.segment, [WhitespaceSegment(context.segment.raw + " ")]) ], )
def _coerce_indent_to( self, desired_indent: str, current_indent_buffer: List[BaseSegment], current_anchor: BaseSegment, ) -> List[LintFix]: """Generate fixes to make an indent a certain size. Rather than blindly creating indent, we should _edit_ if at all possible, this stops other rules trying to remove floating double indents. """ existing_whitespace = [ seg for seg in current_indent_buffer if seg.is_type("whitespace") ] # Should we have an indent? if len(desired_indent) == 0: # No? Just delete everything return [LintFix.delete(seg) for seg in existing_whitespace] else: # Is there already an indent? if existing_whitespace: # Edit the first, delete the rest. edit_fix = LintFix.replace( existing_whitespace[0], [existing_whitespace[0].edit(desired_indent)], ) delete_fixes = [ LintFix.delete(seg) for seg in existing_whitespace[1:] ] return [edit_fix] + delete_fixes else: # Just create an indent. return [ LintFix.create_before( current_anchor, [ WhitespaceSegment(raw=desired_indent, ), ], ) ]
def _column_only_fix_list( context: RuleContext, column_reference_segment: BaseSegment, ) -> List[LintFix]: """Generate list of fixes to reduce CASE statement to a single column.""" fixes = [ LintFix.replace( context.segment, [column_reference_segment], ) ] return fixes
def _handle_semicolon_newline( self, target_segment: RawSegment, parent_segment: BaseSegment, 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( parent_segment, anchor_segment) fixes = [] if anchor_segment is target_segment: fixes.append( LintFix.replace( anchor_segment, [ NewlineSegment(), SymbolSegment(raw=";", type="statement_terminator"), ], )) else: fixes.extend( self._create_semicolon_and_delete_whitespace( target_segment, parent_segment, anchor_segment, info.whitespace_deletions, [ NewlineSegment(), SymbolSegment(raw=";", type="statement_terminator"), ], )) return LintResult( anchor=anchor_segment, fixes=fixes, )
def _eval(self, context: RuleContext) -> Optional[LintResult]: """Looking for DISTINCT before a bracket. Look for DISTINCT keyword immediately followed by open parenthesis. """ # We trigger on `select_clause` and look for `select_clause_modifier` assert context.segment.is_type("select_clause") children = FunctionalContext(context).segment.children() modifier = children.select(sp.is_type("select_clause_modifier")) first_element = children.select( sp.is_type("select_clause_element")).first() if not modifier or not first_element: return None # is the first element only an expression with only brackets? expression = (first_element.children(sp.is_type("expression")).first() or first_element) bracketed = expression.children(sp.is_type("bracketed")).first() if bracketed: fixes = [] # If there's nothing else in the expression, remove the brackets. if len(expression[0].segments) == 1: # Remove the brackets and strip any meta segments. fixes.append( LintFix.replace( bracketed[0], self.filter_meta(bracketed[0].segments)[1:-1]), ) # If no whitespace between DISTINCT and expression, add it. if not children.select(sp.is_whitespace(), start_seg=modifier[0], stop_seg=first_element[0]): fixes.append( LintFix.create_before( first_element[0], [WhitespaceSegment()], )) # If no fixes, no problem. if fixes: return LintResult(anchor=modifier[0], fixes=fixes) return None
def _generate_fixes( operator_new_lines: str, change_list: Segments, operator: BaseSegment, insert_anchor: BaseSegment, ) -> LintResult: # Duplicate the change list and append the operator inserts: List[BaseSegment] = [ *change_list, operator, ] if operator_new_lines == "before": # We do yet another reverse here, # This could be avoided but makes all "changes" relate to "before" config state inserts = [*reversed(inserts)] # ensure to insert in the right place edit_type = "create_before" if operator_new_lines == "before" else "create_after" fixes = [ # Insert elements reversed LintFix( edit_type=edit_type, edit=map(lambda el: copy.deepcopy(el), reversed(inserts)), anchor=insert_anchor, ), # remove the Op LintFix.delete(operator), # Delete the original elements (related to insert) *change_list.apply(LintFix.delete), ] desc = before_description if operator_new_lines == "before" else after_description return LintResult( anchor=operator, description=desc, fixes=fixes, )
def _eval(self, context: RuleContext) -> Optional[LintResult]: """Commas should not have whitespace directly before them.""" if not context.raw_stack: return None # pragma: no cover anchor: Optional[RawSegment] = context.raw_stack[-1] if ( # We need at least one segment previous segment for this to work. anchor is not None and context.segment.is_type("comma") and anchor.is_type("whitespace") and anchor.pos_marker.line_pos > 1 ): return LintResult(anchor=anchor, fixes=[LintFix.delete(anchor)]) # Otherwise fine. return None
def _eval(self, context: RuleContext) -> LintResult: """Unnecessary trailing whitespace. Look for newline segments, and then evaluate what it was preceded by. """ if len(context.raw_stack) > 0 and context.raw_stack[-1].is_type( "whitespace"): # Look for a newline (or file end), which is preceded by whitespace deletions = ( FunctionalContext(context).raw_stack.reversed().select( loop_while=sp.is_type("whitespace"))) # NOTE: The presence of a loop marker should prevent false # flagging of newlines before jinja loop tags. return LintResult( anchor=deletions[-1], fixes=[LintFix.delete(d) for d in deletions], ) return LintResult()
def _eval(self, context: RuleContext) -> Optional[LintResult]: """Files must not begin with newlines or whitespace.""" # Only check raw segments. This ensures we don't try and delete the same # whitespace multiple times (i.e. for non-raw segments higher in the # tree). raw_segments = [] whitespace_types = {"newline", "whitespace", "indent", "dedent"} for seg in context.segment.recursive_crawl_all(): if not seg.is_raw(): continue if seg.is_type(*whitespace_types): raw_segments.append(seg) continue segment = Segments(seg) raw_stack = Segments(*raw_segments, templated_file=context.templated_file) # Non-whitespace segment. if (not raw_stack.all(sp.is_meta()) # Found leaf of parse tree. and not segment.all(sp.is_expandable()) # It is possible that a template segment (e.g. # {{ config(materialized='view') }}) renders to an empty string # and as such is omitted from the parsed tree. We therefore # should flag if a templated raw slice intersects with the # source slices in the raw stack and skip this rule to avoid # risking collisions with template objects. and not raw_stack.raw_slices.any( rsp.is_slice_type("templated"))): return LintResult( anchor=context.segment, fixes=[LintFix.delete(d) for d in raw_stack], ) else: break return None
def _eval(self, context: RuleContext) -> Optional[List[LintResult]]: """Ambiguous ordering directions for columns in order by clause. This rule checks if some ORDER BY columns explicitly specify ASC or DESC and some don't. """ # We only trigger on orderby_clause lint_fixes = [] orderby_spec = self._get_orderby_info(context.segment) order_types = {o.order for o in orderby_spec} # If ALL columns or NO columns explicitly specify ASC/DESC, all is # well. if None not in order_types or order_types == {None}: return None # There's a mix of explicit and default sort order. Make everything # explicit. for col_info in orderby_spec: if not col_info.order: # Since ASC is default in SQL, add in ASC for fix lint_fixes.append( LintFix.create_before( col_info.separator, [WhitespaceSegment(), KeywordSegment("ASC")], )) return [ LintResult( anchor=context.segment, fixes=lint_fixes, description= ("Ambiguous order by clause. Order by clauses should specify " "order direction for ALL columns or NO columns."), ) ]