def on_leave( self, original_node: CSTNodeT, updated_node: CSTNodeT ) -> Union[cst.Import, cst.ImportFrom, CSTNodeT, RemovalSentinel]: if isinstance(updated_node, cst.Import): for alias in updated_node.names: name = alias.name if isinstance(name, cst.Name) and name.value == "b": return cst.RemoveFromParent() elif isinstance(updated_node, cst.ImportFrom): module = updated_node.module if isinstance(module, cst.Name) and module.value == "e": return cst.RemoveFromParent() return updated_node
def leave_Try( self, original_node, updated_node) -> Union[cst.BaseStatement, cst.RemovalSentinel]: for original_handler, updated_handler in zip(original_node.handlers, updated_node.handlers): if self.exec_counts[original_handler.body] > 0: self.insert_statements_before_current( updated_handler.body.body) super().leave_Try(original_node, updated_node) return cst.RemoveFromParent() self.insert_statements_before_current(updated_node.body.body) super().leave_Try(original_node, updated_node) return cst.RemoveFromParent()
def leave_import_alike( self, original_node: Union[cst.Import, cst.ImportFrom], updated_node: Union[cst.Import, cst.ImportFrom], ) -> Union[cst.RemovalSentinel, cst.Import, cst.ImportFrom]: names_to_keep = [] for import_alias in updated_node.names: if isinstance(import_alias.name, cst.Attribute): import_name = self.get_import_name_from_attr( attr_node=import_alias.name ) else: import_name = (import_alias.asname or import_alias).name.value if self.is_import_used( import_name, self.get_location(original_node) ): names_to_keep.append(import_alias) if not names_to_keep: return cst.RemoveFromParent() elif len(updated_node.names) == len(names_to_keep): return updated_node else: names_to_keep[-1] = names_to_keep[-1].with_changes( comma=cst.MaybeSentinel.DEFAULT ) return updated_node.with_changes(names=names_to_keep)
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 leave_ImportFrom( self, original_node: cst.Import, updated_node: cst.Import ) -> Union[cst.Import, cst.RemovalSentinel]: # TODO: Handle star imports? if original_node.module: mod_name = original_node.module elif original_node.relative: mod_name = cst.Name( importlib.util.resolve_name("." * len(original_node.relative), self.pkg_fullname)) for alias in original_node.names: indirect_name = alias.asname.name if alias.asname else alias.name direct_name = alias.name indirect_ref = cst.helpers.parse_template_expression( self.pkg_fullname + ".{name}", name=indirect_name) direct_ref = cst.helpers.parse_template_expression( "{mod}.{name}", mod=mod_name, name=direct_name) indirect_ref_name = cst.helpers.get_full_name_for_node( indirect_ref) if direct_ref.deep_equals(indirect_ref): continue self.rewrites[indirect_ref_name] = direct_ref return cst.RemoveFromParent()
def leave_import_alike( self, original_node: ImportT, updated_node: ImportT, ) -> Union[cst.RemovalSentinel, ImportT]: names_to_keep = [] names = cast(Sequence[cst.ImportAlias], updated_node.names) # already handled by leave_ImportFrom for column, import_alias in enumerate(names): if isinstance(import_alias.name, cst.Attribute): import_name = self.get_import_name_from_attr( attr_node=import_alias.name) else: raw_import = import_alias.asname or import_alias raw_import_name = cst.ensure_type(raw_import.name, cst.Name) import_name = raw_import_name.value if self.is_import_used(import_name, column + 1, self.get_location(original_node)): names_to_keep.append(import_alias) if not names_to_keep: return cst.RemoveFromParent() elif len(names) == len(names_to_keep): return updated_node else: names_to_keep[-1] = names_to_keep[-1].with_changes( comma=cst.MaybeSentinel.DEFAULT) return cast(ImportT, updated_node.with_changes(names=names_to_keep))
def leave_ImportFrom( self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom ) -> Union[cst.ImportFrom, cst.RemovalSentinel]: # Grab the scope for this import. If we don't have scope, we can't determine # whether this import is unused so it is unsafe to remove. scope = self.get_metadata(ScopeProvider, original_node, None) if scope is None: return updated_node # Make sure we have anything to do with this node. names = original_node.names if isinstance(names, cst.ImportStar): # This is a star import, so we won't remove it. return updated_node # Make sure we actually know the absolute module. module_name = get_absolute_module_for_import( self.context.full_module_name, updated_node ) if module_name is None or module_name not in self.unused_obj_imports: # This node isn't on our list of todos, so let's bail. return updated_node objects_to_remove = self.unused_obj_imports[module_name] names_to_keep = [] for import_alias in names: # Figure out if it is in our list of things to kill for name, alias in objects_to_remove: if ( name == import_alias.evaluated_name and alias == import_alias.evaluated_alias ): break else: # This is a keeper, we don't have it on our list. names_to_keep.append(import_alias) continue # Now that we know we want to remove this object, figure out if # there are any live references to it. if self._is_in_use(scope, import_alias): names_to_keep.append(import_alias) continue # no changes if names_to_keep == names: return updated_node # Now, either remove this statement or remove the imports we are # deleting from this statement. if len(names_to_keep) == 0: return cst.RemoveFromParent() if names_to_keep[-1] != names[-1]: # Remove trailing comma in order to not mess up import statements. names_to_keep = [ *names_to_keep[:-1], names_to_keep[-1].with_changes(comma=cst.MaybeSentinel.DEFAULT), ] return updated_node.with_changes(names=names_to_keep)
def on_leave( self, original_node: CSTNodeT, updated_node: CSTNodeT ) -> Union[CSTNodeT, RemovalSentinel]: if isinstance(updated_node, cst.Continue): return cst.RemoveFromParent() else: return updated_node
def leave_Assign(self, original_node, updated_node): if m.matches(original_node, obj_new_pattern): var = original_node.targets[0].target.value if var in self.globls and id( self.globls[var]) in self.objs_to_inline: return cst.RemoveFromParent() return updated_node
def _leave_foo_bar( self, original_node: cst.SimpleStatementLine, updated_node: cst.SimpleStatementLine, ) -> cst.RemovalSentinel: RemoveImportsVisitor.remove_unused_import_by_node( self.context, original_node) return cst.RemoveFromParent()
def test_replace_simple_sentinel(self) -> None: # Verify behavior when there's a sentinel as a replacement original = cst.parse_module( "def bar(x: int, y: int) -> bool:\n return False\n") replaced = cst.ensure_type( m.replace(original, m.Param(), cst.RemoveFromParent()), cst.Module).code self.assertEqual(replaced, "def bar() -> bool:\n return False\n")
def leave_Expr(self, original_node, updated_node): final_node = super().leave_Expr(original_node, updated_node) if is_pure(final_node.value): if m.matches(final_node, m.Expr(m.SimpleString())): s = final_node.value.value if s.startswith('"""'): return final_node return cst.RemoveFromParent() return final_node
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_Import( self, original_node: cst.Import, updated_node: cst.Import ) -> Union[cst.Import, cst.RemovalSentinel]: name_idxs_to_remove = [] for n, alias in enumerate(original_node.names): if alias.asname: # If an `import ...` has an alias, then it simply needs to be # replaced, because that alias will necessarily serve as a type # of indirect import. indirect_ref = cst.helpers.parse_template_expression( self.pkg_fullname + ".{name}", name=alias.asname.name) direct_ref = alias.name indirect_ref_name = cst.helpers.get_full_name_for_node( indirect_ref) self.rewrites[indirect_ref_name] = direct_ref name_idxs_to_remove.append(n) else: module_fullname = cst.helpers.get_full_name_for_node( alias.name) module_package = self.pkg_info.fullnames_to_packages[ module_fullname] if module_package == self.pkg_fullname: pass elif module_package >= self.pkg_fullname: # The imported object is in a sub-package of this package # We could remove it, but that would require new `import` # statements in the modules that use this direct reference. # TODO: This seems like a good optional functionality to # offer. pass else: # This import is for an object above this package level, # but it's not an aliased import, so it can't be an # indirect reference, but it could definitely be # introducing some unwanted (sub-)package dependencies. pass if name_idxs_to_remove: new_names = tuple(name for n, name in enumerate(updated_node.names) if n not in name_idxs_to_remove) if not new_names: return cst.RemoveFromParent() updated_node = updated_node.with_changes(names=new_names) return updated_node
def on_leave(self, original_node, updated_node): final_node = super().on_leave(original_node, updated_node) if (isinstance(final_node, cst.BaseStatement) and not m.matches( final_node, m.SimpleStatementLine(body=[m.Expr(m.SimpleString())])) and self.exec_counts[original_node] == 0): return cst.RemoveFromParent() return final_node
def leave_If(self, original_node, updated_node): then_branch_count = self.exec_counts[original_node.body] # If then was always taken, just return then branch if then_branch_count == self.block_execs[-1]: self.insert_statements_before_current(updated_node.body.body) super().leave_If(original_node, updated_node) return cst.RemoveFromParent() # If else was always taken, just return else branch elif updated_node.orelse is not None and then_branch_count == 0: self.insert_statements_before_current( self.reattach_comments(updated_node.orelse, list(updated_node.orelse.body.body))) self.dont_keep_comments() super().leave_If(original_node, updated_node) return cst.RemoveFromParent() return super().leave_If(original_node, updated_node)
def leave_If(self, original_node, updated_node): final_node = super().leave_If(original_node, updated_node) if not isinstance(final_node, cst.RemovalSentinel): if (final_node.orelse is not None and len(final_node.orelse.body.body) == 0): final_node = final_node.with_changes(orelse=None) if len(final_node.body.body) == 0: return cst.RemoveFromParent() return final_node
def leave_ExtSlice( self, original_node: cst.ExtSlice, updated_node: cst.ExtSlice ) -> Union[cst.ExtSlice, cst.RemovalSentinel]: slc = updated_node.slice if isinstance(slc, cst.Index): val = slc.value if isinstance(val, cst.Name): if val.value == "DoNotCareSentinel": # We don't support maybes in matchers. return cst.RemoveFromParent() return updated_node
def leave_SubscriptElement( self, original_node: cst.SubscriptElement, updated_node: cst.SubscriptElement ) -> Union[cst.SubscriptElement, cst.RemovalSentinel]: slc = updated_node.slice if isinstance(slc, cst.Index): val = slc.value if isinstance(val, cst.Name): if val.value in self.values: # This type matches, so out it goes return cst.RemoveFromParent() return updated_node
def leave_EmptyLine( self, original_node: libcst.EmptyLine, updated_node: libcst.EmptyLine ) -> Union[libcst.EmptyLine, libcst.RemovalSentinel]: if updated_node.comment is None or not bool( self._regex_pattern.search( libcst.ensure_type(updated_node.comment, libcst.Comment).value)): # This is a normal comment return updated_node # This is a directive comment matching our tag, so remove it. return libcst.RemoveFromParent()
def leave_StarImport( updated_node: cst.ImportFrom, imp: ImportFrom, ) -> Union[cst.ImportFrom, cst.RemovalSentinel]: if imp.suggestions: names_to_suggestions = [ cst.ImportAlias(cst.Name(module)) for module in imp.suggestions ] return updated_node.with_changes(names=names_to_suggestions) else: return cst.RemoveFromParent()
def leave_AnnAssign(self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign): if updated_node.value is None: # e.g. `some_var: str` # these are *only* type declarations and have no runtime behavior, # so they should be removed entirely: return cst.RemoveFromParent() return cst.Assign( targets=[cst.AssignTarget(target=updated_node.target)], value=updated_node.value)
def leave_SubscriptElement( self, original_node: cst.SubscriptElement, updated_node: cst.SubscriptElement ) -> Union[cst.SubscriptElement, cst.RemovalSentinel]: slc = updated_node.slice if isinstance(slc, cst.Index): val = slc.value if isinstance(val, cst.Name): if "Sentinel" in val.value: # We don't support maybes in matchers. return cst.RemoveFromParent() # Simple trick to kill trailing commas return updated_node.with_changes(comma=cst.MaybeSentinel.DEFAULT)
def leave_StarImport(self, original_node, updated_node, **kwargs): imp = kwargs["imp"] if imp["modules"]: modules = ",".join(imp["modules"]) names_to_suggestion = [] for module in modules.split(","): names_to_suggestion.append(cst.ImportAlias(cst.Name(module))) return updated_node.with_changes(names=names_to_suggestion) else: if imp["module"]: return cst.RemoveFromParent() return original_node
def leave_import_alike(self, original_node, updated_node): names_to_keep = [] for import_alias in updated_node.names: if isinstance(import_alias.name, cst.Attribute): import_name = self.get_import_name_from_attr( attr_node=import_alias.name) else: import_name = (import_alias.asname or import_alias).name.value if self.is_import_used(import_name, self.get_location(original_node)): names_to_keep.append( import_alias.with_changes(comma=cst.MaybeSentinel.DEFAULT)) if len(names_to_keep) == 0: return cst.RemoveFromParent() else: return updated_node.with_changes(names=names_to_keep)
def leave_Import( self, original_node: cst.Import, updated_node: cst.Import ) -> Union[cst.Import, cst.RemovalSentinel]: # Grab the scope for this import. If we don't have scope, we can't determine # whether this import is unused so it is unsafe to remove. scope = self.get_metadata(ScopeProvider, original_node, None) if scope is None: return updated_node names_to_keep = [] for import_alias in original_node.names: if import_alias.evaluated_name not in self.unused_module_imports: # This is a keeper since we aren't removing it names_to_keep.append(import_alias) continue if ( import_alias.evaluated_alias != self.unused_module_imports[import_alias.evaluated_name] ): # This is a keeper since the alias does not match # what we are looking for. names_to_keep.append(import_alias) continue # Now that we know we want to remove this module, figure out if # there are any live references to it. if self._is_in_use(scope, import_alias): names_to_keep.append(import_alias) continue # no changes if names_to_keep == original_node.names: return updated_node # Now, either remove this statement or remove the imports we are # deleting from this statement. if len(names_to_keep) == 0: return cst.RemoveFromParent() if names_to_keep[-1] != original_node.names[-1]: # Remove trailing comma in order to not mess up import statements. names_to_keep = [ *names_to_keep[:-1], names_to_keep[-1].with_changes(comma=cst.MaybeSentinel.DEFAULT), ] return updated_node.with_changes(names=names_to_keep)
def leave_Import(self, original_node: cst.Import, updated_node: cst.Import) -> cst.Import: code = self.get_metadata(cst.metadata.PositionProvider, original_node) new_import_alias = [] line_no = code.start.line for import_alias in updated_node.names: if self.is_reimport(line_no, import_alias): continue new_import_alias.append(import_alias) if new_import_alias: new_import_alias[-1] = new_import_alias[-1].with_changes( comma=cst.MaybeSentinel.DEFAULT) return updated_node.with_changes(names=new_import_alias) if len(new_import_alias) == 0: return cst.RemoveFromParent() return updated_node
def leave_Expr( self, original_node: "Expr", updated_node: "Expr" ) -> Union["BaseSmallStatement", RemovalSentinel]: # FIXME: For some strange reason if we put that matcher combination # into m.call_if_inside() (or others), then it will get triggered on # _every_ function call. Not sure if bug or a feature :/ if m.matches( original_node, m.Expr(value=m.Call(func=m.OneOf( m.Attribute( value=m.OneOf(m.Name(value="pdb"), m.Name( value="ipdb")), attr=m.Name(value="set_trace"), ), m.Name("breakpoint"), ))), ): return cst.RemoveFromParent() return original_node
def leave_Assign(self, original_node, updated_node): if any([ m.matches( updated_node, m.Assign(targets=[m.AssignTarget(m.Name())], value=pattern)) for pattern in self.rhs_patterns ]): var = original_node.targets[0].target scope = self.get_metadata(ScopeProvider, var) children = self._scope_children[scope] if len(scope.assignments[var]) == 1: valid_scopes = [scope] + [ child for child in children if len(child.assignments[var]) == 0 ] self.propagate(valid_scopes, var, updated_node.value) return cst.RemoveFromParent() return updated_node
def leave_ImportFrom(self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom) -> cst.ImportFrom: code = self.get_metadata(cst.metadata.PositionProvider, original_node) line_no = code.start.line new_import_alias = [] if isinstance(updated_node.names, cst.ImportStar): # we do not handle ImportStar return updated_node for import_alias in updated_node.names: if self.is_reimport_from(line_no, updated_node.module, import_alias): continue new_import_alias.append(import_alias) if new_import_alias: new_import_alias[-1] = new_import_alias[-1].with_changes( comma=cst.MaybeSentinel.DEFAULT) return updated_node.with_changes(names=new_import_alias) if len(new_import_alias) == 0: return cst.RemoveFromParent() return updated_node