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"))), ))
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))
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
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)
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)
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), )
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=[])