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 _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, )
def _handle_semicolon_same_line( self, target_segment: RawSegment, parent_segment: BaseSegment, info: SegmentMoveContext, ) -> Optional[LintResult]: if not info.before_segment: return None # If preceding segments are found then delete the old # semi-colon and its preceding whitespace and then insert # the semi-colon in the correct location. fixes = self._create_semicolon_and_delete_whitespace( target_segment, parent_segment, info.anchor_segment, info.whitespace_deletions, [ SymbolSegment(raw=";", type="statement_terminator"), ], ) return LintResult( anchor=info.anchor_segment, fixes=fixes, )
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 = SymbolSegment(",", name="comma", 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
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 make_symbol(cls, raw, seg_type, name=None): """Make a symbol segment.""" # For the name of the segment, we force the string to lowercase. symbol_seg = SymbolSegment.make(raw.lower(), name=name or seg_type, type=seg_type) # At the moment we let the rule dictate *case* here. return symbol_seg(raw=raw, pos_marker=None)
def _handle_semicolon_same_line( context: RuleContext, info: SegmentMoveContext) -> Optional[LintResult]: if not info.before_segment: return None # If preceding segments are found then delete the old # semi-colon and its preceding whitespace and then insert # the semi-colon in the correct location. fixes = [ LintFix.replace( info.anchor_segment, [ info.anchor_segment, SymbolSegment(raw=";", type="symbol", name="semicolon"), ], ), LintFix.delete(context.segment, ), ] fixes.extend(LintFix.delete(d) for d in info.whitespace_deletions) return LintResult( anchor=info.anchor_segment, fixes=fixes, )
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.", )
exasol_fs_dialect.add( FunctionScriptTerminatorSegment=NamedSegment.make( "function_script_terminator", type="statement_terminator" ), WalrusOperatorSegment=NamedSegment.make( "walrus_operator", type="assignment_operator" ), VariableNameSegment=ReSegment.make( r"[A-Z][A-Z0-9_]*", name="function_variable", type="variable", ), ) exasol_fs_dialect.replace( SemicolonSegment=SymbolSegment.make(";", name="semicolon", type="semicolon"), ) @exasol_fs_dialect.segment(replace=True) class StatementSegment(BaseSegment): """A generic segment, to any of its child subsegments.""" type = "statement" match_grammar = GreedyUntil(Ref("FunctionScriptTerminatorSegment")) parse_grammar = OneOf( Ref("CreateFunctionStatementSegment"), Ref("CreateScriptingLuaScriptStatementSegment"), Ref("CreateUDFScriptStatementSegment"), Ref("CreateAdapterScriptStatementSegment"),
def _ensure_final_semicolon(self, context: RuleContext) -> Optional[LintResult]: # Locate the end of the file. if not self.is_final_segment(context): return None # Include current segment for complete stack. complete_stack: List[BaseSegment] = list(context.raw_stack) complete_stack.append(context.segment) # Iterate backwards over complete stack to find # if the final semi-colon is already present. anchor_segment = context.segment semi_colon_exist_flag = False is_one_line = False before_segment = [] for segment in complete_stack[::-1]: if segment.name == "semicolon": semi_colon_exist_flag = True elif segment.is_code: is_one_line = self._is_one_line_statement(context, segment) break elif not segment.is_meta: before_segment.append(segment) anchor_segment = segment semicolon_newline = self.multiline_newline if not is_one_line else False if not semi_colon_exist_flag: # Create the final semi-colon if it does not yet exist. # Semi-colon on same line. if not semicolon_newline: fixes = [ LintFix.replace( anchor_segment, [ anchor_segment, SymbolSegment( raw=";", type="symbol", name="semicolon"), ], ) ] # Semi-colon on new line. else: # Adjust before_segment and anchor_segment for inline # comments. ( before_segment, anchor_segment, ) = self._handle_preceding_inline_comments( before_segment, anchor_segment) fixes = [ LintFix.replace( anchor_segment, [ anchor_segment, NewlineSegment(), SymbolSegment( raw=";", type="symbol", name="semicolon"), ], ) ] return LintResult( anchor=anchor_segment, fixes=fixes, ) return None
snowflake_dialect.sets("reserved_keywords").update([ "CLONE", "MASKING", "NOTIFICATION", "PIVOT", "SAMPLE", "TABLESAMPLE", "UNPIVOT", ]) snowflake_dialect.add( # In snowflake, these are case sensitive even though they're not quoted # so they need a different `name` and `type` so they're not picked up # by other rules. ParameterAssignerSegment=SymbolSegment.make("=>", name="parameter_assigner", type="parameter_assigner"), NakedSemiStructuredElementSegment=ReSegment.make( r"[A-Z0-9_]*", name="naked_semi_structured_element", type="semi_structured_element", ), QuotedSemiStructuredElementSegment=NamedSegment.make( "double_quote", name="quoted_semi_structured_element", type="semi_structured_element", ), ColumnIndexIdentifierSegment=ReSegment.make( r"\$[0-9]+", name="column_index_identifier_segment", type="identifier"), )
def _ensure_final_semicolon( self, parent_segment: BaseSegment) -> Optional[LintResult]: # Iterate backwards over complete stack to find # if the final semi-colon is already present. anchor_segment = parent_segment.segments[-1] trigger_segment = parent_segment.segments[-1] semi_colon_exist_flag = False is_one_line = False before_segment = [] for segment in parent_segment.segments[::-1]: anchor_segment = segment if segment.is_type("statement_terminator"): semi_colon_exist_flag = True elif segment.is_code: is_one_line = self._is_one_line_statement( parent_segment, segment) break elif not segment.is_meta: before_segment.append(segment) trigger_segment = segment self.logger.debug("Trigger on: %s", trigger_segment) self.logger.debug("Anchoring on: %s", anchor_segment) semicolon_newline = self.multiline_newline if not is_one_line else False if not semi_colon_exist_flag: # Create the final semi-colon if it does not yet exist. # Semi-colon on same line. if not semicolon_newline: fixes = [ LintFix.create_after( self._choose_anchor_segment( parent_segment, "create_after", anchor_segment, filter_meta=True, ), [ SymbolSegment(raw=";", type="statement_terminator"), ], ) ] # Semi-colon on new line. else: # Adjust before_segment and anchor_segment for inline # comments. ( before_segment, anchor_segment, ) = self._handle_preceding_inline_comments( before_segment, anchor_segment) self.logger.debug("Revised anchor on: %s", anchor_segment) fixes = [ LintFix.create_after( self._choose_anchor_segment( parent_segment, "create_after", anchor_segment, filter_meta=True, ), [ NewlineSegment(), SymbolSegment(raw=";", type="statement_terminator"), ], ) ] return LintResult( anchor=trigger_segment, fixes=fixes, ) return None
), ] + exasol_fs_dialect.get_lexer_struct()) exasol_fs_dialect.add( FunctionScriptTerminatorSegment=NamedSegment.make( "function_script_terminator", type="statement_terminator"), WalrusOperatorSegment=NamedSegment.make("walrus_operator", type="assignment_operator"), VariableNameSegment=ReSegment.make( r"[A-Z][A-Z0-9_]*", name="function_variable", type="variable", ), ) exasol_fs_dialect.replace(SemicolonSegment=SymbolSegment.make( ";", name="semicolon", type="semicolon"), ) @exasol_fs_dialect.segment(replace=True) class StatementSegment(BaseSegment): """A generic segment, to any of its child subsegments.""" type = "statement" match_grammar = GreedyUntil(Ref("FunctionScriptTerminatorSegment")) parse_grammar = OneOf( Ref("CreateFunctionStatementSegment"), Ref("CreateScriptingLuaScriptStatementSegment"), Ref("CreateUDFScriptStatementSegment"), Ref("CreateAdapterScriptStatementSegment"), )