def discard_empty_else_blocks(self, _, updated_node): # An `else: pass` block can always simply be discarded, and libcst ensures # that an Else node can only ever occur attached to an If, While, For, or Try # node; in each case `None` is the valid way to represent "no else block". if m.findall(updated_node, m.Comment()): return updated_node # If there are any comments, keep the node return cst.RemoveFromParent()
def __init__(self, context: CstContext) -> None: super().__init__(context) config = self.context.config.rule_config.get(self.__class__.__name__, None) if config is None: self.rule_disabled: bool = True else: if not isinstance(config, dict) or "header" not in config: raise ValueError( "A ``header`` str config is required by AddMissingHeaderRule." ) header_str = config["header"] if not isinstance(header_str, str): raise ValueError( "A ``header`` str config is required by AddMissingHeaderRule." ) lines = header_str.split("\n") self.header_matcher: List[m.EmptyLine] = [ m.EmptyLine(comment=m.Comment(value=line)) for line in lines ] self.header_replacement: List[cst.EmptyLine] = [ cst.EmptyLine(comment=cst.Comment(value=line)) for line in lines ] if "path" in config: path_pattern = config["path"] if not isinstance(path_pattern, str): raise ValueError( "``path`` config should be a str in AddMissingHeaderRule." ) else: path_pattern = "*.py" self.rule_disabled = not self.context.file_path.match(path_pattern)
def has_footer_comment(body): return m.matches( body, m.IndentedBlock(footer=[ m.ZeroOrMore(), m.EmptyLine(comment=m.Comment()), m.ZeroOrMore() ]), )
def leave_EmptyLine( self, original_node: libcst.EmptyLine, updated_node: libcst.EmptyLine ) -> Union[libcst.EmptyLine, libcst.RemovalSentinel]: # First, find misplaced lines. for tag in self.PYRE_TAGS: if m.matches(updated_node, m.EmptyLine(comment=m.Comment(f"# pyre-{tag}"))): if self.in_module_header: # We only want to remove this if we've already found another # pyre-strict in the header (that means its duplicated). We # also don't want to move the pyre-strict since its already in # the header, so don't mark that we need to move. self.module_header_tags[tag] += 1 if self.module_header_tags[tag] > 1: return libcst.RemoveFromParent() else: return updated_node else: # This showed up outside the module header, so move it inside if self.module_header_tags[tag] < 1: self.move_strict[tag] = True return libcst.RemoveFromParent() # Now, find misnamed lines if m.matches(updated_node, m.EmptyLine(comment=m.Comment(f"# pyre {tag}"))): if self.in_module_header: # We only want to remove this if we've already found another # pyre-strict in the header (that means its duplicated). We # also don't want to move the pyre-strict since its already in # the header, so don't mark that we need to move. self.module_header_tags[tag] += 1 if self.module_header_tags[tag] > 1: return libcst.RemoveFromParent() else: return updated_node.with_changes( comment=libcst.Comment(f"# pyre-{tag}")) else: # We found an intended pyre-strict, but its spelled wrong. So, remove it # and re-add a new one in leave_Module. if self.module_header_tags[tag] < 1: self.move_strict[tag] = True return libcst.RemoveFromParent() # We found a regular comment, don't care about this. return updated_node
def leave_Call( # noqa: C901 self, original_node: cst.Call, updated_node: cst.Call) -> cst.BaseExpression: # Lets figure out if this is a "".format() call extraction = self.extract( updated_node, m.Call(func=m.Attribute( value=m.SaveMatchedNode(m.SimpleString(), "string"), attr=m.Name("format"), )), ) if extraction is not None: fstring: List[cst.BaseFormattedStringContent] = [] inserted_sequence: int = 0 stringnode = cst.ensure_type(extraction["string"], cst.SimpleString) tokens = _get_tokens(stringnode.raw_value) for (literal_text, field_name, format_spec, conversion) in tokens: if literal_text: fstring.append(cst.FormattedStringText(literal_text)) if field_name is None: # This is not a format-specification continue if format_spec is not None and len(format_spec) > 0: # TODO: This is supportable since format specs are compatible # with f-string format specs, but it would require matching # format specifier expansions. self.warn( f"Unsupported format_spec {format_spec} in format() call" ) return updated_node # Auto-insert field sequence if it is empty if field_name == "": field_name = str(inserted_sequence) inserted_sequence += 1 expr = _find_expr_from_field_name(field_name, updated_node.args) if expr is None: # Most likely they used * expansion in a format. self.warn( f"Unsupported field_name {field_name} in format() call" ) return updated_node # Verify that we don't have any comments or newlines. Comments aren't # allowed in f-strings, and newlines need parenthesization. We can # have formattedstrings inside other formattedstrings, but I chose not # to doeal with that for now. if self.findall(expr, m.Comment()): # We could strip comments, but this is a formatting change so # we choose not to for now. self.warn(f"Unsupported comment in format() call") return updated_node if self.findall(expr, m.FormattedString()): self.warn(f"Unsupported f-string in format() call") return updated_node if self.findall(expr, m.Await()): # This is fixed in 3.7 but we don't currently have a flag # to enable/disable it. self.warn(f"Unsupported await in format() call") return updated_node # Stripping newlines is effectively a format-only change. expr = cst.ensure_type( expr.visit(StripNewlinesTransformer(self.context)), cst.BaseExpression, ) # Try our best to swap quotes on any strings that won't fit expr = cst.ensure_type( expr.visit( SwitchStringQuotesTransformer(self.context, stringnode.quote[0])), cst.BaseExpression, ) # Verify that the resulting expression doesn't have a backslash # in it. raw_expr_string = self.module.code_for_node(expr) if "\\" in raw_expr_string: self.warn(f"Unsupported backslash in format expression") return updated_node # For safety sake, if this is a dict/set or dict/set comprehension, # wrap it in parens so that it doesn't accidentally create an # escape. if (raw_expr_string.startswith("{") or raw_expr_string.endswith("}")) and (not expr.lpar or not expr.rpar): expr = expr.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) # Verify that any strings we insert don't have the same quote quote_gatherer = StringQuoteGatherer(self.context) expr.visit(quote_gatherer) for stringend in quote_gatherer.stringends: if stringend in stringnode.quote: self.warn( f"Cannot embed string with same quote from format() call" ) return updated_node fstring.append( cst.FormattedStringExpression(expression=expr, conversion=conversion)) return cst.FormattedString( parts=fstring, start=f"f{stringnode.prefix}{stringnode.quote}", end=stringnode.quote, ) return updated_node
def remove_trailing_comma(node): # Remove the comma from this node, *unless* it's already a comma node with comments if node.comma is cst.MaybeSentinel.DEFAULT or m.findall(node, m.Comment()): return node return node.with_changes(comma=cst.MaybeSentinel.DEFAULT)
class Error(typing.NamedTuple): model_file_path: str line: int col: int field: str def __str__(self) -> str: return (f'{self.model_file_path}:{self.line}:{self.col} ' f'Field "{self.field}" needs a valid deprecation comment') def is_model_field_type(name: str) -> bool: return name.endswith(('Field', 'ForeignKey')) _any_comment = m.TrailingWhitespace(comment=m.Comment( m.MatchIfTrue(lambda n: n.startswith('#'))), newline=m.Newline()) _django_model_field_name_value = m.Call(func=m.Attribute( attr=m.Name(m.MatchIfTrue(is_model_field_type)))) | m.Call( func=m.Name(m.MatchIfTrue(is_model_field_type))) _django_model_field_name_with_leading_comment_value = m.Call( func=m.Attribute(attr=m.Name(m.MatchIfTrue(is_model_field_type))), whitespace_before_args=m.ParenthesizedWhitespace(_any_comment), ) | m.Call( func=m.Name(m.MatchIfTrue(is_model_field_type)), whitespace_before_args=m.ParenthesizedWhitespace(_any_comment), ) _django_model_field_with_leading_comment = m.SimpleStatementLine(body=[
from libcst import matchers as m from libcst.metadata import MetadataWrapper, PositionProvider from hooks.utils.pre_commit import get_input_files VALID_COMMENTS_FOR_NULL_TRUE = {'null_by_design', 'null_for_compatibility'} Error = namedtuple('Error', 'line, col, field') def is_valid_comment(comment_text: str) -> bool: return any(item in comment_text for item in VALID_COMMENTS_FOR_NULL_TRUE) null_comment = m.TrailingWhitespace( comment=m.Comment(m.MatchIfTrue(is_valid_comment)), newline=m.Newline(), ) field_without_comment = m.SimpleStatementLine( body=[ m.Assign(value=(m.Call( args=[ m.ZeroOrMore(), m.Arg(keyword=m.Name('null'), value=m.Name('True')), m.ZeroOrMore(), ], whitespace_before_args=m.DoesNotMatch( m.ParenthesizedWhitespace(null_comment)), ) | m.Call(
from collections import namedtuple from typing import Iterator, List, cast import libcst from libcst import Assign, SimpleStatementLine from libcst import matchers as m from libcst.metadata import MetadataWrapper, PositionProvider from hooks.utils.pre_commit import get_input_files VALID_COMMENTS_FOR_NULL_TRUE = {'# null_by_design', '# null_for_compatibility'} Error = namedtuple('Error', 'line, col, field') null_comment = m.TrailingWhitespace( comment=m.Comment( m.MatchIfTrue(lambda n: n in VALID_COMMENTS_FOR_NULL_TRUE)), newline=m.Newline(), ) field_without_comment = m.SimpleStatementLine( body=[ m.Assign(value=(m.Call( args=[ m.ZeroOrMore(), m.Arg(keyword=m.Name('null'), value=m.Name('True')), m.ZeroOrMore(), ], whitespace_before_args=m.DoesNotMatch( m.ParenthesizedWhitespace(null_comment)), ) | m.Call(
def _convert_token_to_fstring_expression( self, field_name: str, conversion: Optional[str], arguments: Sequence[cst.Arg], containing_string: cst.SimpleString, ) -> Optional[cst.FormattedStringExpression]: expr = _find_expr_from_field_name(field_name, arguments) if expr is None: # Most likely they used * expansion in a format. self.warn(f"Unsupported field_name {field_name} in format() call") return None # Verify that we don't have any comments or newlines. Comments aren't # allowed in f-strings, and newlines need parenthesization. We can # have formattedstrings inside other formattedstrings, but I chose not # to doeal with that for now. if self.findall(expr, m.Comment()) and not self.allow_strip_comments: # We could strip comments, but this is a formatting change so # we choose not to for now. self.warn("Unsupported comment in format() call") return None if self.findall(expr, m.FormattedString()): self.warn("Unsupported f-string in format() call") return None if self.findall(expr, m.Await()) and not self.allow_await: # This is fixed in 3.7 but we don't currently have a flag # to enable/disable it. self.warn("Unsupported await in format() call") return None # Stripping newlines is effectively a format-only change. expr = cst.ensure_type( expr.visit(StripNewlinesTransformer(self.context)), cst.BaseExpression, ) # Try our best to swap quotes on any strings that won't fit expr = cst.ensure_type( expr.visit( SwitchStringQuotesTransformer(self.context, containing_string.quote[0])), cst.BaseExpression, ) # Verify that the resulting expression doesn't have a backslash # in it. raw_expr_string = self.module.code_for_node(expr) if "\\" in raw_expr_string: self.warn("Unsupported backslash in format expression") return None # For safety sake, if this is a dict/set or dict/set comprehension, # wrap it in parens so that it doesn't accidentally create an # escape. if (raw_expr_string.startswith("{") or raw_expr_string.endswith("}")) and (not expr.lpar or not expr.rpar): expr = expr.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) # Verify that any strings we insert don't have the same quote quote_gatherer = StringQuoteGatherer(self.context) expr.visit(quote_gatherer) for stringend in quote_gatherer.stringends: if stringend in containing_string.quote: self.warn( "Cannot embed string with same quote from format() call") return None return cst.FormattedStringExpression(expression=expr, conversion=conversion)