/** * @name Useless assignment to local variable * @description An assignment to a local variable that is not used later on, or whose value is always * overwritten, has no effect. * @kind problem * @problem.severity warning * @id cs/useless-assignment-to-local * @tags maintainability * external/cwe/cwe-563 * @precision very-high */ import csharp /** * Gets a callable that either directly captures local variable `v`, or which * is enclosed by the callable that declares `v` and encloses a callable that * captures `v`. */ Callable getACapturingCallableAncestor(LocalVariable v) { result = v.getACapturingCallable() or exists(Callable mid | mid = getACapturingCallableAncestor(v) | result = mid.getEnclosingCallable() and not v.getEnclosingCallable() = result ) } Expr getADelegateExpr(Callable c) { c = result.(CallableAccess).getTarget() or result = c.(AnonymousFunctionExpr) } /** * Holds if `c` is a call where any delegate argument is evaluated immediately. */ predicate nonEscapingCall(Call c) { exists(string name | c.getTarget().hasName(name) | name = "ForEach" or name = "Count" or name = "Any" or name = "All" or name = "Average" or name = "Aggregate" or name = "First" or name = "Last" or name = "FirstOrDefault" or name = "LastOrDefault" or name = "LongCount" or name = "Max" or name = "Single" or name = "SingleOrDefault" or name = "Sum" ) } /** * Holds if `v` is a captured local variable, and one of the callables capturing * `v` may escape the local scope. */ predicate mayEscape(LocalVariable v) { exists(Callable c, Expr e, Expr succ | c = getACapturingCallableAncestor(v) | e = getADelegateExpr(c) and DataFlow::localFlow(DataFlow::exprNode(e), DataFlow::exprNode(succ)) and not succ = any(DelegateCall dc).getDelegateExpr() and not succ = any(Cast cast).getExpr() and not succ = any(Call call | nonEscapingCall(call)).getAnArgument() and not succ = any(AssignableDefinition ad | ad.getTarget() instanceof LocalVariable).getSource() ) } class RelevantDefinition extends AssignableDefinition { RelevantDefinition() { this instanceof AssignableDefinitions::AssignmentDefinition or this instanceof AssignableDefinitions::MutationDefinition or this instanceof AssignableDefinitions::TupleAssignmentDefinition // Discards in out assignments are only possible from C# 7 (2017), so we disable this case // for now //or //this.(AssignableDefinitions::OutRefDefinition).getTargetAccess().isOutArgument() or this.(AssignableDefinitions::LocalVariableDefinition).getDeclaration() = any(LocalVariableDeclExpr lvde | lvde = any(SpecificCatchClause scc).getVariableDeclExpr() or lvde = any(ForeachStmt fs).getVariableDeclExpr() ) or this instanceof AssignableDefinitions::IsPatternDefinition or this instanceof AssignableDefinitions::TypeCasePatternDefinition } /** Holds if this assignment may be live. */ private predicate isMaybeLive() { exists(LocalVariable v | v = this.getTarget() | // SSA definitions are only created for live variables this = any(Ssa::ExplicitDefinition ssaDef).getADefinition() or mayEscape(v) ) } /** Holds if this definition is a variable initializer, for example `string s = null`. */ private predicate isInitializer() { this.getSource() = this.getTarget().(LocalVariable).getInitializer() } /** * Holds if this definition is a default-like variable initializer, for example * `string s = null` or `int i = 0`, but not `string s = "Hello"`. */ private predicate isDefaultLikeInitializer() { this.isInitializer() and exists(Expr e | e = this.getSource() | exists(string val | val = e.getValue() | val = "0" or val = "-1" or val = "" or val = "false" ) or e instanceof NullLiteral or e = any(Field f | f.isStatic() and (f.isReadOnly() or f.isConst())).getAnAccess() or e instanceof DefaultValueExpr or e instanceof AnonymousObjectCreation ) } /** Holds if this definition is dead and we want to report it. */ predicate isDead() { // Ensure that the definition is not in dead code exists(this.getAControlFlowNode()) and not this.isMaybeLive() and ( // Allow dead initializer assignments, such as `string s = string.Empty`, but only // if the initializer expression assigns a default-like value, and there exists another // definition of the same variable this.isInitializer() implies ( not this.isDefaultLikeInitializer() or not exists(AssignableDefinition other | other.getTarget() = this.getTarget() | other != this ) ) ) } } from RelevantDefinition def, LocalVariable v where v = def.getTarget() and def.isDead() select def, "This assignment to $@ is useless, since its value is never read.", v, v.getName()