Exemple #1
0
 def test_does_not_match_true(self) -> None:
     # Match on any call that takes one argument that isn't the value None.
     self.assertTrue(
         matches(
             libcst.Call(libcst.Name("foo"),
                         (libcst.Arg(libcst.Name("True")), )),
             m.Call(args=(m.Arg(value=m.DoesNotMatch(m.Name("None"))), )),
         ))
     self.assertTrue(
         matches(
             libcst.Call(libcst.Name("foo"),
                         (libcst.Arg(libcst.Integer("1")), )),
             m.Call(args=(m.DoesNotMatch(m.Arg(m.Name("None"))), )),
         ))
     self.assertTrue(
         matches(
             libcst.Call(libcst.Name("foo"),
                         (libcst.Arg(libcst.Integer("1")), )),
             m.Call(args=m.DoesNotMatch((m.Arg(m.Integer("2")), ))),
         ))
     # Match any call that takes an argument which isn't True or False.
     self.assertTrue(
         matches(
             libcst.Call(libcst.Name("foo"),
                         (libcst.Arg(libcst.Integer("1")), )),
             m.Call(args=(m.Arg(value=m.DoesNotMatch(
                 m.OneOf(m.Name("True"), m.Name("False")))), )),
         ))
     # Match any name node that doesn't match the regex for True
     self.assertTrue(
         matches(
             libcst.Name("False"),
             m.Name(value=m.DoesNotMatch(m.MatchRegex(r"True"))),
         ))
Exemple #2
0
    def test_predicate_logic(self) -> None:
        # Verify that we can or things together.
        matcher = m.BinaryOperation(left=m.OneOf(
            m.MatchMetadata(meta.PositionProvider,
                            self._make_coderange((1, 0), (1, 1))),
            m.MatchMetadata(meta.PositionProvider,
                            self._make_coderange((1, 0), (1, 2))),
        ))
        node, wrapper = self._make_fixture("a + b")
        self.assertTrue(matches(node, matcher, metadata_resolver=wrapper))
        node, wrapper = self._make_fixture("12 + 3")
        self.assertTrue(matches(node, matcher, metadata_resolver=wrapper))
        node, wrapper = self._make_fixture("123 + 4")
        self.assertFalse(matches(node, matcher, metadata_resolver=wrapper))

        # Verify that we can and things together
        matcher = m.BinaryOperation(left=m.AllOf(
            m.MatchMetadata(meta.PositionProvider,
                            self._make_coderange((1, 0), (1, 1))),
            m.MatchMetadata(meta.ExpressionContextProvider,
                            meta.ExpressionContext.LOAD),
        ))
        node, wrapper = self._make_fixture("a + b")
        self.assertTrue(matches(node, matcher, metadata_resolver=wrapper))
        node, wrapper = self._make_fixture("ab + cd")
        self.assertFalse(matches(node, matcher, metadata_resolver=wrapper))

        # Verify that we can not things
        matcher = m.BinaryOperation(left=m.DoesNotMatch(
            m.MatchMetadata(meta.ExpressionContextProvider,
                            meta.ExpressionContext.STORE)))
        node, wrapper = self._make_fixture("a + b")
        self.assertTrue(matches(node, matcher, metadata_resolver=wrapper))
Exemple #3
0
class GatherCommentsVisitor(ContextAwareVisitor):
    """
    Collects all comments matching a certain regex and their line numbers.
    This visitor is useful for capturing special-purpose comments, for example
    ``noqa`` style lint suppression annotations.

    Standalone comments are assumed to affect the line following them, and
    inline ones are recorded with the line they are on.

    After visiting a CST, matching comments are collected in the ``comments``
    attribute.
    """

    METADATA_DEPENDENCIES = (PositionProvider, )

    def __init__(self, context: CodemodContext, comment_regex: str) -> None:
        super().__init__(context)

        #: Dictionary of comments found in the CST. Keys are line numbers,
        #: values are comment nodes.
        self.comments: Dict[int, cst.Comment] = {}

        self._comment_matcher: Pattern[str] = re.compile(comment_regex)

    @m.visit(m.EmptyLine(comment=m.DoesNotMatch(None)))
    @m.visit(m.TrailingWhitespace(comment=m.DoesNotMatch(None)))
    def visit_comment(
            self, node: Union[cst.EmptyLine, cst.TrailingWhitespace]) -> None:
        comment = node.comment
        assert comment is not None  # hello, type checker
        if not self._comment_matcher.match(comment.value):
            return
        line = self.get_metadata(PositionProvider, comment).start.line
        if isinstance(node, cst.EmptyLine):
            # Standalone comments refer to the next line
            line += 1
        self.comments[line] = comment
Exemple #4
0
    def leave_Call(self, original_node, updated_node):
        """Convert positional to keyword arguments."""
        metadata = self.get_metadata(cst.metadata.QualifiedNameProvider,
                                     original_node)
        qualnames = {qn.name for qn in metadata}

        # If this isn't one of our known functions, or it has no posargs, stop there.
        if (len(qualnames) != 1
                or not qualnames.intersection(self.kwonly_functions)
                or not m.matches(
                    updated_node,
                    m.Call(
                        func=m.DoesNotMatch(m.Call()),
                        args=[m.Arg(keyword=None),
                              m.ZeroOrMore()],
                    ),
                )):
            return updated_node

        # Get the actual function object so that we can inspect the signature.
        # This does e.g. incur a dependency on Numpy to fix Numpy-dependent code,
        # but having a single source of truth about the signatures is worth it.
        params = signature(get_fn(*qualnames)).parameters.values()

        # st.floats() has a new allow_subnormal kwonly argument not at the end,
        # so we do a bit more of a dance here.
        if qualnames == {"hypothesis.strategies.floats"}:
            params = [p for p in params if p.name != "allow_subnormal"]

        if len(updated_node.args) > len(params):
            return updated_node

        # Create new arg nodes with the newly required keywords
        assign_nospace = cst.AssignEqual(
            whitespace_before=cst.SimpleWhitespace(""),
            whitespace_after=cst.SimpleWhitespace(""),
        )
        newargs = [
            arg if arg.keyword or arg.star
            or p.kind is not Parameter.KEYWORD_ONLY else arg.with_changes(
                keyword=cst.Name(p.name), equal=assign_nospace)
            for p, arg in zip(params, updated_node.args)
        ]
        return updated_node.with_changes(args=newargs)
Exemple #5
0
    def leave_Call(self, original_node, updated_node):
        """Convert positional to keyword arguments."""
        metadata = self.get_metadata(cst.metadata.QualifiedNameProvider, original_node)
        qualnames = {qn.name for qn in metadata}

        # If this isn't one of our known functions, or it has no posargs, stop there.
        if (
            len(qualnames) != 1
            or not qualnames.intersection(self.kwonly_functions)
            or not m.matches(
                updated_node,
                m.Call(
                    func=m.DoesNotMatch(m.Call()),
                    args=[m.Arg(keyword=None), m.ZeroOrMore()],
                ),
            )
        ):
            return updated_node

        # Get the actual function object so that we can inspect the signature.
        # This does e.g. incur a dependency on Numpy to fix Numpy-dependent code,
        # but having a single source of truth about the signatures is worth it.
        mod, fn = list(qualnames.intersection(self.kwonly_functions))[0].rsplit(".", 1)
        try:
            func = getattr(importlib.import_module(mod), fn)
        except ImportError:
            return updated_node

        # Create new arg nodes with the newly required keywords
        assign_nospace = cst.AssignEqual(
            whitespace_before=cst.SimpleWhitespace(""),
            whitespace_after=cst.SimpleWhitespace(""),
        )
        newargs = [
            arg
            if arg.keyword or arg.star or p.kind is not Parameter.KEYWORD_ONLY
            else arg.with_changes(keyword=cst.Name(p.name), equal=assign_nospace)
            for p, arg in zip(signature(func).parameters.values(), updated_node.args)
        ]
        return updated_node.with_changes(args=newargs)
Exemple #6
0
 def test_inverse_inverse_is_identity(self) -> None:
     # Verify that we don't wrap an InverseOf in an InverseOf in normal circumstances.
     identity = m.Name("True")
     self.assertTrue(m.DoesNotMatch(m.DoesNotMatch(identity)) is identity)
     self.assertTrue((~(~identity)) is identity)

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(
                            func=m.Attribute(attr=m.Name('NullBooleanField')),
                            whitespace_before_args=m.DoesNotMatch(
                                m.ParenthesizedWhitespace(null_comment)),
                        )
                        | m.Call(
                            func=m.Name('NullBooleanField'),
                            whitespace_before_args=m.DoesNotMatch(
                                m.ParenthesizedWhitespace(null_comment)),
                        )))
    ],
    trailing_whitespace=m.DoesNotMatch(null_comment),
)
Exemple #8
0
class GatherUnusedImportsVisitor(ContextAwareVisitor):
    """
    Collects all imports from a module not directly used in the same module.
    Intended to be instantiated and passed to a :class:`libcst.Module`
    :meth:`~libcst.CSTNode.visit` method to process the full module.

    Note that imports that are only used indirectly (from other modules) are
    still collected.

    After visiting a module the attribute ``unused_imports`` will contain a
    set of unused :class:`~libcst.ImportAlias` objects, paired with their
    parent import node.
    """

    METADATA_DEPENDENCIES: Tuple[ProviderT] = (
        *GatherNamesFromStringAnnotationsVisitor.METADATA_DEPENDENCIES,
        ScopeProvider,
    )

    def __init__(self, context: CodemodContext) -> None:
        super().__init__(context)

        self._string_annotation_names: Set[str] = set()
        self._exported_names: Set[str] = set()
        #: Contains a set of (alias, parent_import) pairs that are not used
        #: in the module after visiting.
        self.unused_imports: Set[Tuple[cst.ImportAlias,
                                       Union[cst.Import,
                                             cst.ImportFrom]]] = set()

    def visit_Module(self, node: cst.Module) -> bool:
        export_collector = GatherExportsVisitor(self.context)
        node.visit(export_collector)
        self._exported_names = export_collector.explicit_exported_objects
        annotation_visitor = GatherNamesFromStringAnnotationsVisitor(
            self.context)
        node.visit(annotation_visitor)
        self._string_annotation_names = annotation_visitor.names
        return True

    @m.visit(m.Import()
             | m.ImportFrom(
                 module=m.DoesNotMatch(m.Name("__future__")),
                 names=m.DoesNotMatch(m.ImportStar()),
             ))
    def handle_import(self, node: Union[cst.Import, cst.ImportFrom]) -> None:
        names = node.names
        assert not isinstance(names, cst.ImportStar)  # hello, type checker

        for alias in names:
            self.unused_imports.add((alias, node))

    def leave_Module(self, original_node: cst.Module) -> None:
        self.unused_imports = self.filter_unused_imports(self.unused_imports)

    def filter_unused_imports(
        self,
        candidates: Iterable[Tuple[cst.ImportAlias, Union[cst.Import,
                                                          cst.ImportFrom]]],
    ) -> Set[Tuple[cst.ImportAlias, Union[cst.Import, cst.ImportFrom]]]:
        """
        Return the imports in ``candidates`` which are not used.

        This function implements the main logic of this visitor, and is called after traversal. It calls :meth:`~is_in_use` on each import.

        Override this in a subclass for additional filtering.
        """
        unused_imports = set()
        for (alias, parent) in candidates:
            scope = self.get_metadata(ScopeProvider, parent)
            if scope is None:
                continue
            if not self.is_in_use(scope, alias):
                unused_imports.add((alias, parent))
        return unused_imports

    def is_in_use(self, scope: cst.metadata.Scope,
                  alias: cst.ImportAlias) -> bool:
        """
        Check if ``alias`` is in use in the given ``scope``.

        An alias is in use if it's directly referenced, exported, or appears in
        a string type annotation. Override this in a subclass for additional
        filtering.
        """
        asname = alias.asname
        names = _gen_dotted_names(
            cst.ensure_type(asname.name, cst.Name
                            ) if asname is not None else alias.name)

        for name_or_alias, _ in names:
            if (name_or_alias in self._exported_names
                    or name_or_alias in self._string_annotation_names):
                return True

            for assignment in scope[name_or_alias]:
                if (isinstance(assignment, cst.metadata.Assignment)
                        and isinstance(assignment.node,
                                       (cst.ImportFrom, cst.Import))
                        and len(assignment.references) > 0):
                    return True
        return False
class RemoveParenthesesFromReturn(cst.CSTTransformer):
    @m.call_if_inside(m.Return(value=m.Tuple()))
    @m.leave(m.DoesNotMatch(m.Tuple(lpar=[])))
    def leave_Tuple(self, original_node: "Tuple",
                    updated_node: "Tuple") -> "Tuple":
        return original_node.with_changes(lpar=[], rpar=[])