/**
 * @name Capturing a foreach variable
 * @description Code that captures a 'foreach' variable and uses it outside the loop behaves differently in C# version 4 and C# version 5
 * @kind problem
 * @problem.severity recommendation
 * @precision medium
 * @id cs/captured-foreach-variable
 * @tags portability
 *       maintainability
 *       language-features
 *       external/cwe/cwe-758
 */

import csharp
import semmle.code.csharp.dataflow.LibraryTypeDataFlow
import semmle.code.csharp.frameworks.system.Collections
import semmle.code.csharp.frameworks.system.collections.Generic

predicate lambdaCaptures(AnonymousFunctionExpr lambda, Variable v) {
  exists(VariableAccess va | va.getEnclosingCallable() = lambda | va.getTarget() = v)
}

predicate lambdaCapturesLoopVariable(AnonymousFunctionExpr lambda, ForeachStmt loop, Variable v) {
  lambdaCaptures(lambda, v) and
  inForeachStmtBody(loop, lambda) and
  loop.getVariable() = v
}

predicate inForeachStmtBody(ForeachStmt loop, Element e) {
  e = loop.getBody()
  or
  exists(Element mid |
    inForeachStmtBody(loop, mid) and
    e = mid.getAChild()
  )
}

class LambdaDataFlowConfiguration extends DataFlow::Configuration {
  LambdaDataFlowConfiguration() { this = "LambdaDataFlowConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    lambdaCapturesLoopVariable(source.asExpr(), _, _)
  }

  override predicate isSink(DataFlow::Node sink) { exists(getAssignmentTarget(sink.asExpr())) }

  predicate capturesLoopVarAndIsStoredIn(
    AnonymousFunctionExpr lambda, Variable loopVar, Element storage
  ) {
    exists(DataFlow::Node sink | this.hasFlow(DataFlow::exprNode(lambda), sink) |
      storage = getAssignmentTarget(sink.asExpr())
    ) and
    exists(ForeachStmt loop | lambdaCapturesLoopVariable(lambda, loop, loopVar) |
      not declaredInsideLoop(loop, storage)
    )
  }
}

Element getAssignmentTarget(Expr e) {
  exists(Assignment a | a.getRValue() = e |
    result = a.getLValue().(PropertyAccess).getTarget() or
    result = a.getLValue().(FieldAccess).getTarget() or
    result = a.getLValue().(LocalVariableAccess).getTarget() or
    result = a.getLValue().(EventAccess).getTarget()
  )
  or
  exists(AddEventExpr aee |
    e = aee.getRValue() and
    result = aee.getLValue().getTarget()
  )
  or
  result = getCollectionAssignmentTarget(e)
}

Element getCollectionAssignmentTarget(Expr e) {
  // Store into collection via method
  exists(
    MethodCall mc, Method m, LibraryTypeDataFlow ltdf, CallableFlowSource source,
    CallableFlowSink sink
  |
    m = mc.getTarget().getSourceDeclaration() and
    ltdf.callableFlow(source, AccessPath::empty(), sink, AccessPath::element(), m, _) and
    e = source.getSource(mc) and
    result.(Variable).getAnAccess() = sink.getSink(mc)
  )
  or
  // Array initializer
  e = result.(ArrayCreation).getInitializer().getAnElement()
  or
  // Collection initializer
  e =
    result
        .(ObjectCreation)
        .getInitializer()
        .(CollectionInitializer)
        .getElementInitializer(_)
        .getAnArgument()
  or
  // Store values using indexer
  exists(IndexerAccess ia, AssignExpr ae |
    ia.getQualifier() = result.(Variable).getAnAccess() and
    ia = ae.getLValue() and
    e = ae.getRValue()
  )
}

// Variable v is declared inside the loop body
predicate declaredInsideLoop(ForeachStmt loop, LocalVariable v) {
  exists(LocalVariableDeclStmt decl | decl.getVariableDeclExpr(_).getVariable() = v |
    inForeachStmtBody(loop, decl)
  )
}

from LambdaDataFlowConfiguration c, AnonymousFunctionExpr lambda, Variable loopVar, Element storage
where c.capturesLoopVarAndIsStoredIn(lambda, loopVar, storage)
select lambda, "Function which may be stored in $@ captures variable $@", storage,
  storage.toString(), loopVar, loopVar.getName()
