Пример #1
0
 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()
Пример #2
0
 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)
Пример #3
0
def has_footer_comment(body):
    return m.matches(
        body,
        m.IndentedBlock(footer=[
            m.ZeroOrMore(),
            m.EmptyLine(comment=m.Comment()),
            m.ZeroOrMore()
        ]),
    )
Пример #4
0
 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
Пример #5
0
    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
Пример #6
0
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)
Пример #7
0
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(
Пример #9
0
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(
Пример #10
0
    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)