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
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
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
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 _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, WhitespaceSegment( raw=desired_indent, ), ) ] # 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], WhitespaceSegment( raw=desired_indent, ), ) ] return fixes
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
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()
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()
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, 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) < 1: return None # Get the first element of this segment. first_elem = next(segment.iter_raw_seg()) cm1 = raw_stack[-1] if cm1.name == "comma": # comma followed by something that isn't whitespace? if first_elem.name not in ["whitespace", "newline", "Dedent"]: self.logger.debug( "Comma followed by something other than whitespace: %s", first_elem) ins = WhitespaceSegment(raw=" ") return LintResult( anchor=cm1, fixes=[LintFix("edit", segment, [ins, segment])]) if len(raw_stack) < 2: return None cm2 = raw_stack[-2] if cm2.name == "comma": # comma followed by too much whitespace? if (cm1.is_whitespace # Must be whitespace and cm1.raw != " " # ...and not a single one and cm1.name != "newline" # ...and not a newline and not first_elem. is_comment # ...and not followed by a comment ): self.logger.debug("Comma followed by too much whitespace: %s", cm1) repl = WhitespaceSegment(raw=" ") return LintResult(anchor=cm1, fixes=[LintFix("edit", cm1, repl)]) # Otherwise we're fine return None
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 _create_base_is_null_sequence( is_upper: bool, operator_raw: str, ) -> CorrectionListType: is_seg = KeywordSegment("IS" if is_upper else "is") not_seg = KeywordSegment("NOT" if is_upper else "not") if operator_raw == "=": return [is_seg] return [ is_seg, WhitespaceSegment(), not_seg, ]
def _coerce_indent_to( self, desired_indent: str, current_indent_buffer: Tuple[RawSegment, ...], current_anchor: BaseSegment, ) -> List[LintFix]: """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_before( current_anchor, [ WhitespaceSegment( raw=desired_indent, ), ], ), ] # Otherwise edit the first element to be the right size else: # Edit the first element of this line's indent and remove any other # indents. fixes = [ LintFix.replace( current_indent_buffer[0], [ WhitespaceSegment( raw=desired_indent, ), ], ), ] + [LintFix.delete(elem) for elem in current_indent_buffer[1:]] return fixes
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
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.is_type("expression"): segments_filtered = self.filter_meta(segment.segments) if segments_filtered and segments_filtered[0].is_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].is_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, [WhitespaceSegment(raw=insert_str)], )) return LintResult(anchor=segment, fixes=fixes) return None
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 = [ WhitespaceSegment(), select_modifier, NewlineSegment(), ] 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, )
def test__parser__grammar_anysetof(generate_test_segments): """Test the AnySetOf grammar.""" token_list = ["bar", " \t ", "foo", " \t ", "bar"] seg_list = generate_test_segments(token_list) bs = StringParser("bar", KeywordSegment) fs = StringParser("foo", KeywordSegment) g = AnySetOf(fs, bs) with RootParseContext(dialect=None) as ctx: # Check directly assert g.match(seg_list, parse_context=ctx).matched_segments == ( KeywordSegment("bar", seg_list[0].pos_marker), WhitespaceSegment(" \t ", seg_list[1].pos_marker), KeywordSegment("foo", seg_list[2].pos_marker), ) # Check with a bit of whitespace assert not g.match(seg_list[1:], parse_context=ctx)
def _eval(self, segment, raw_stack, **kwargs): """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` if segment.is_type("select_clause"): modifier = segment.get_child("select_clause_modifier") if not modifier: return None first_element = segment.get_child("select_clause_element") if not first_element: return None # is the first element only an expression with only brackets? expression = first_element.get_child("expression") if not expression: expression = first_element bracketed = expression.get_child("bracketed") if not bracketed: return None fixes = [] # If there's nothing else in the expression, remove the brackets. if len(expression.segments) == 1: # Remove the brackets, and strip any meta segments. fixes = [ LintFix( "edit", bracketed, self.filter_meta(bracketed.segments)[1:-1] ), ] # Is there any whitespace between DISTINCT and the expression? distinct_idx = segment.segments.index(modifier) elem_idx = segment.segments.index(first_element) if not any( seg.is_whitespace for seg in segment.segments[distinct_idx:elem_idx] ): fixes.append( LintFix( "create", first_element, WhitespaceSegment(), ) ) # If no fixes, no problem. if fixes: return LintResult(anchor=modifier, fixes=fixes) return None
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 _eval(self, segment, raw_stack, **kwargs): """Incorrect indentation found in file.""" tab = "\t" space = " " correct_indent = ( space * self.tab_space_size if self.indent_unit == "space" else tab ) wrong_indent = ( tab if self.indent_unit == "space" else space * self.tab_space_size ) if segment.is_type("whitespace") and wrong_indent in segment.raw: fixes = [] description = "Incorrect indentation type found in file." edit_indent = segment.raw.replace(wrong_indent, correct_indent) # 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 segment.raw.count(space) % self.tab_space_size == 0 ) # Only attempt a fix at the start of a newline for now and (len(raw_stack) == 0 or raw_stack[-1].is_type("newline")) ): fixes = [ LintFix( "edit", segment, WhitespaceSegment(raw=edit_indent), ) ] elif not (len(raw_stack) == 0 or raw_stack[-1].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=segment, fixes=fixes, description=description)
def _eval(self, segment, parent_stack, **kwargs): """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 if segment.is_type("orderby_clause"): lint_fixes = [] orderby_spec = self._get_orderby_info(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", col_info.separator, [WhitespaceSegment(), KeywordSegment("ASC")], )) return [ LintResult( anchor=segment, fixes=lint_fixes, description= ("Ambiguous order by clause. Order by " "clauses should specify order direction for ALL columns or NO columns." ), ) ] return None
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 _eval(self, segment, **kwargs): """Single whitespace expected in mother segment between pre and post segments.""" error_buffer = [] if segment.is_type(self.expected_mother_segment_type): last_code = None mid_segs = [] for seg in 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: # There's nothing between. Just add a whitespace fixes = [ LintFix( "create", seg, [WhitespaceSegment()], ) ] else: # Don't otherwise suggest a fix for now. # 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 _eval(self, segment, parent_stack, **kwargs): """Unnecessary whitespace.""" # For the given segment, lint whitespace directly within it. prev_newline = True prev_whitespace = None violations = [] 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 != " ": violations.append( LintResult( anchor=prev_whitespace, fixes=[ LintFix( "edit", prev_whitespace, WhitespaceSegment(), ) ], )) prev_newline = False prev_whitespace = None if violations: return violations
def _eval(self, segment, raw_stack, **kwargs): """WITH clause closing bracket should be aligned with WITH keyword. Look for a with clause and evaluate the position of closing brackets. """ # We only trigger on start_bracket (open parenthesis) if segment.is_type("with_compound_statement"): raw_stack_buff = list(raw_stack) # Look for the with keyword for seg in segment.segments: if seg.name.lower() == "with": seg_line_no = seg.pos_marker.line_no break else: # pragma: no cover # This *could* happen if the with statement is unparsable, # in which case then the user will have to fix that first. if any(s.is_type("unparsable") for s in segment.segments): return LintResult() # If it's parsable but we still didn't find a with, then # we should raise that. raise RuntimeError("Didn't find WITH keyword!") def indent_size_up_to(segs): seg_buff = [] # Get any segments running up to the WITH for elem in reversed(segs): if elem.is_type("newline"): break elif elem.is_meta: continue else: seg_buff.append(elem) # reverse the indent if we have one if seg_buff: seg_buff = list(reversed(seg_buff)) indent_str = "".join(seg.raw for seg in seg_buff).replace( "\t", " " * self.tab_space_size) indent_size = len(indent_str) return indent_size, indent_str balance = 0 with_indent, with_indent_str = indent_size_up_to(raw_stack_buff) for seg in segment.iter_segments( expanding=["common_table_expression", "bracketed"], pass_through=True): if seg.name == "start_bracket": balance += 1 elif seg.name == "end_bracket": balance -= 1 if balance == 0: closing_bracket_indent, _ = indent_size_up_to( raw_stack_buff) indent_diff = closing_bracket_indent - with_indent # Is indent of closing bracket not the same as # indent of WITH keyword. if seg.pos_marker.line_no == seg_line_no: # Skip if it's the one-line version. That's ok pass elif indent_diff < 0: return LintResult( anchor=seg, fixes=[ LintFix( "create", seg, WhitespaceSegment(" " * (-indent_diff)), ) ], ) elif indent_diff > 0: # Is it all whitespace before the bracket on this line? prev_segs_on_line = [ elem for elem in segment.iter_segments( expanding=[ "common_table_expression", "bracketed" ], pass_through=True, ) if elem.pos_marker.line_no == seg.pos_marker. line_no and elem.pos_marker.line_pos < seg.pos_marker.line_pos ] if all( elem.is_type("whitespace") for elem in prev_segs_on_line): # We can move it back, it's all whitespace fixes = [ LintFix( "create", seg, [WhitespaceSegment(with_indent_str)], ) ] + [ LintFix("delete", elem) for elem in prev_segs_on_line ] else: # We have to move it to a newline fixes = [ LintFix( "create", seg, [ NewlineSegment(), WhitespaceSegment(with_indent_str), ], ) ] return LintResult(anchor=seg, fixes=fixes) else: raw_stack_buff.append(seg) return LintResult()
base_module="test.fixtures.rules.custom.bad_rule_name", ) e.match("Rule classes must be named in the format of") def test_rule_set_return_informative_error_when_rule_not_registered(): """Assert that a rule that throws an exception returns it as a validation.""" cfg = FluffConfig(overrides={"dialect": "ansi"}) with pytest.raises(ValueError) as e: get_rule_from_set("L000", config=cfg) e.match("'L000' not in") seg = WhitespaceSegment(pos_marker=PositionMarker( slice(0, 1), slice(0, 1), TemplatedFile(" ", fname="<str>"))) @pytest.mark.parametrize( "lint_result, expected", [ (LintResult(), "LintResult(<empty>)"), (LintResult(seg), "LintResult(<WhitespaceSegment: ([L: 1, P: 1]) ' '>)"), ( LintResult(seg, description="foo"), "LintResult(foo: <WhitespaceSegment: ([L: 1, P: 1]) ' '>)", ), ( LintResult( seg,
def _eval_single_select_target_element(self, select_targets_info, select_clause, parent_stack): is_wildcard = False for segment in select_clause.segments: if segment.is_type("select_clause_element"): for sub_segment in segment.segments: if sub_segment.is_type("wildcard_expression"): is_wildcard = True if is_wildcard: return None elif (select_targets_info.select_idx < select_targets_info.first_new_line_idx < select_targets_info.first_select_target_idx): # Do we have a modifier? modifier = select_clause.get_child("select_clause_modifier") # Prepare the select clause which will be inserted # In most (but not all) case we'll want to replace the newline with # the statement and a newline, but in some cases however (see #1424) # we don't need the final newline. copy_with_newline = True insert_buff = [ WhitespaceSegment(), select_clause.segments[ select_targets_info.first_select_target_idx], ] # Check if the modifier is one we care about if modifier: # If it's already on the first line, ignore it. if (select_clause.segments.index(modifier) < select_targets_info.first_new_line_idx): modifier = None fixes = [ # Delete the first select target from its original location. # We'll add it to the right section at the end, once we know # what to add. LintFix( "delete", select_clause.segments[ select_targets_info.first_select_target_idx], ), ] start_idx = 0 # If we have a modifier to move: if modifier: # Add it to the insert insert_buff = [WhitespaceSegment(), modifier] + insert_buff modifier_idx = select_clause.segments.index(modifier) # Delete the whitespace after it (which is two after, thanks to indent) if (len(select_clause.segments) > modifier_idx + 1 and select_clause.segments[modifier_idx + 2].is_whitespace): fixes += [ LintFix( "delete", select_clause.segments[modifier_idx + 2], ), ] # Delete the modifier itself fixes += [ LintFix( "delete", modifier, ), ] # Set the position marker for removing the preceding # whitespace and newline, which we'll use below. start_idx = modifier_idx else: # Set the position marker for removing the preceding # whitespace and newline, which we'll use below. start_idx = select_targets_info.first_select_target_idx if parent_stack and parent_stack[-1].is_type("select_statement"): select_stmt = parent_stack[-1] select_clause_idx = select_stmt.segments.index(select_clause) after_select_clause_idx = select_clause_idx + 1 if len(select_stmt.segments) > after_select_clause_idx: if select_stmt.segments[after_select_clause_idx].is_type( "newline"): # The select_clause is immediately followed by a # newline. Delete the newline in order to avoid leaving # behind an empty line after fix. delete_last_newline = True # Since, we're deleting the newline, we should also delete all # whitespace before it or it will add random whitespace to # following statements. So walk back through the segment # deleting whitespace until you get the previous newline, or # something else. idx = 1 while start_idx - idx < len(select_clause.segments): # Delete any whitespace if select_clause.segments[start_idx - idx].is_type( "whitespace"): fixes += [ LintFix( "delete", select_clause.segments[start_idx - idx], ), ] # Once we see a newline, then we're done if select_clause.segments[start_idx - idx].is_type( "newline", ): break # If we see anything other than whitespace, # then we're done, but in this case we want to # keep the final newline. if not select_clause.segments[start_idx - idx].is_type( "whitespace", "newline"): delete_last_newline = False break idx += 1 # Finally delete the newline, unless we've decided not to if delete_last_newline: fixes.append( LintFix( "delete", select_stmt. segments[after_select_clause_idx], )) elif select_stmt.segments[after_select_clause_idx].is_type( "whitespace"): # The select_clause has stuff after (most likely a comment) # Delete the whitespace immeadiately after the select clause # so the other stuff aligns nicely based on where the select # clause started fixes += [ LintFix( "delete", select_stmt.segments[after_select_clause_idx], ), ] elif select_stmt.segments[after_select_clause_idx].is_type( "dedent"): # The end of the select statement, so this is the one # case we don't want the newline added to end of # select_clause (see #1424) copy_with_newline = False # Again let's strip back the whitespace, bnut simpler # as don't need to worry about new line so just break # if see non-whitespace idx = 1 start_idx = select_clause_idx - 1 while start_idx - idx < len(select_clause.segments): # Delete any whitespace if select_clause.segments[start_idx - idx].is_type( "whitespace"): fixes += [ LintFix( "delete", select_clause.segments[start_idx - idx], ), ] # Once we see a newline, then we're done if select_clause.segments[start_idx - idx].is_type("newline"): break # If we see anything other than whitespace, # then we're done, but in this case we want to # keep the final newline. if not select_clause.segments[start_idx - idx].is_type( "whitespace", "newline"): copy_with_newline = True break idx += 1 if copy_with_newline: insert_buff = insert_buff + [NewlineSegment()] fixes += [ # Insert the select_clause in place of the first newlin in the # Select statement LintFix( "edit", select_clause.segments[ select_targets_info.first_new_line_idx], insert_buff, ), ] return LintResult( anchor=select_clause, fixes=fixes, ) else: return None
def _eval(self, segment, **kwargs): """Find rule violations and provide fixes. 0. Look for a case expression 1. Find the first expression and "then" 2. Determine if the "then" is followed by a boolean 3. If so, determine if the first then-bool is followed by an else-bool 4. If so, delete everything but the first expression 5a. If then-true-else-false * return deletions * wrap with coalesce 5b. If then-false-else-true * return deletions * add a not condition * wrap with parenthesis and coalesce """ # Look for a case expression if segment.is_type("case_expression") and segment.segments[0].name == "case": # Find the first expression and "then" idx = 0 while segment.segments[idx].name != "then": if segment.segments[idx].is_type("expression"): expression_idx = idx idx += 1 # Determine if "then" is followed by a boolean then_bool_type = None while segment.segments[idx].name not in ["when", "else", "end"]: if segment.segments[idx].raw_upper in ["TRUE", "FALSE"]: then_bool_type = segment.segments[idx].raw_upper idx += 1 if then_bool_type: # Determine if the first then-bool is followed by an else-bool while segment.segments[idx].name != "else": # If the first then-bool is followed by a "WHEN" or "END", exit if segment.segments[idx].name in ["when", "end"]: return None idx += 1 # pragma: no cover # Determine if "else" is followed by a boolean else_bool_type = None while segment.segments[idx].name != "end": if segment.segments[idx].raw_upper in ["TRUE", "FALSE"]: else_bool_type = segment.segments[idx].raw_upper idx += 1 # If then-bool-else-bool, return fixes if ( then_bool_type is not None and else_bool_type is not None and then_bool_type != else_bool_type ): # Generate list of segments to delete -- everything but the # first expression. delete_segments = [] for s in segment.segments: if s != segment.segments[expression_idx]: delete_segments.append(s) # If then-false, add "not" and space edits = [] if then_bool_type == "FALSE": edits.extend( [ KeywordSegment("not"), WhitespaceSegment(), ] ) # Add coalesce and parenthesis edits.extend( [ KeywordSegment("coalesce"), SymbolSegment("(", name="start_bracket", type="start_bracket"), ] ) edit_coalesce_target = segment.segments[0] fixes = [] fixes.append( LintFix( "edit", edit_coalesce_target, edits, ) ) # Add comma, bool, closing parenthesis expression = segment.segments[expression_idx + 1] closing_parenthesis = [ SymbolSegment(",", name="comma", type="comma"), WhitespaceSegment(), KeywordSegment("false"), SymbolSegment(")", name="end_bracket", type="end_bracket"), ] fixes.append( LintFix( "edit", expression, closing_parenthesis, ) ) # Generate a "delete" action for each segment in # delete_segments EXCEPT the one being edited to become a call # to "coalesce(". Deleting and editing the same segment has # unpredictable behavior. fixes += [ LintFix("delete", s) for s in delete_segments if s is not edit_coalesce_target ] return LintResult( anchor=segment.segments[expression_idx], fixes=fixes, description="Case when returns booleans.", )
def make_whitespace(cls, raw): """Make a whitespace segment.""" return WhitespaceSegment(raw=raw, pos_marker=None)