Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detecting direct access to base class state (4th defect type) #48

Merged
merged 20 commits into from
May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
665183a
feat(analyzer.stateaccess): First working version of the analyzer
Leosimetti Apr 3, 2022
e6f40b9
feat(analysis.stateaccess): integration with other analyzers
Leosimetti Apr 3, 2022
9fe9bb1
minor fixes
nikololiahim Apr 4, 2022
e0663ea
docs(analysis.stateaccess)
Leosimetti Apr 4, 2022
9f6d0a2
Merge branch 'direct_access_to_state_defect' of https://github.com/po…
Leosimetti Apr 4, 2022
67b5f6b
feat(analysis.stateAccess): support of `cage` as a possible state
Leosimetti Apr 10, 2022
3df5cc5
fix(docs.stateAccess): Added `self` to examples
Leosimetti Apr 10, 2022
69d67fb
docs(stateAccess): updated to match the latest specifications
Leosimetti Apr 10, 2022
f2d2e74
feat(analysis.stateAccess): support for defect detection in nested ob…
Leosimetti Apr 10, 2022
ac59137
feat(analysis.stateAccess): support for more obscure defect locations
Leosimetti Apr 10, 2022
09d39c1
ref(analysis.stateAccess): remove unnecessary main method
Leosimetti Apr 10, 2022
1621565
test(analysis.stateAccess): 12 test cases
Leosimetti Apr 10, 2022
22218d7
docs(analysis.stateAccess): updated to match latest functionality
Leosimetti Apr 10, 2022
41444b1
ref(analysis.stateAccess): scalaFix
Leosimetti Apr 10, 2022
5ea598e
docs(analysis.stateAccess): minor fixes
Leosimetti Apr 10, 2022
3722747
Revert "docs(analysis.stateAccess)
Leosimetti May 8, 2022
03deacf
test(analysis.directAccess): even more tests
Leosimetti May 9, 2022
112df1e
Merge branch 'master' into direct_access_to_state_defect
Leosimetti May 15, 2022
bb84255
Fix(analysis.Results): merge conflicts
Leosimetti May 15, 2022
8f36dc4
Ref(all): scalafmt
Leosimetti May 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package org.polystat.odin.analysis

import cats._
import cats.data.EitherNel
import cats.data.{EitherNel, NonEmptyList}
import cats.effect.Sync
import cats.syntax.all._
import fs2.Stream
import org.polystat.odin.analysis.EOOdinAnalyzer._
import org.polystat.odin.analysis.liskov.Analyzer
import org.polystat.odin.analysis.mutualrec.advanced.Analyzer.analyzeAst
import org.polystat.odin.analysis.mutualrec.naive.findMutualRecursionFromAst
import org.polystat.odin.analysis.stateaccess.DetectStateAccess
import org.polystat.odin.analysis.utils.inlining.Inliner
import org.polystat.odin.core.ast.EOProg
import org.polystat.odin.core.ast.astparams.EOExprOnly
Expand All @@ -29,9 +30,9 @@ object EOOdinAnalyzer {

final case class Ok(override val ruleId: String) extends OdinAnalysisResult

final case class DefectDetected(
final case class DefectsDetected(
override val ruleId: String,
message: String
messages: NonEmptyList[String],
) extends OdinAnalysisResult

final case class AnalyzerFailure(
Expand All @@ -42,10 +43,10 @@ object EOOdinAnalyzer {
def fromErrors(
analyzer: String
)(errors: List[String]): OdinAnalysisResult =
if (errors.isEmpty)
Ok(analyzer)
else
DefectDetected(analyzer, errors.mkString("\n"))
errors match {
case e :: es => DefectsDetected(analyzer, NonEmptyList(e, es))
case Nil => Ok(analyzer)
}

def fromThrow[F[_]: ApplicativeThrow](
analyzer: String
Expand Down Expand Up @@ -99,7 +100,10 @@ object EOOdinAnalyzer {
})
} yield odinError

stream.compile.toList.map(OdinAnalysisResult.fromErrors(name))
stream
.compile
.toList
.map(OdinAnalysisResult.fromErrors(name))
}

}
Expand Down Expand Up @@ -127,14 +131,6 @@ object EOOdinAnalyzer {

override val name: String = "Unjustified Assumption"

private def toThrow[A](eitherNel: EitherNel[String, A]): F[A] = {
MonadThrow[F].fromEither(
eitherNel
.leftMap(_.mkString_(util.Properties.lineSeparator))
.leftMap(new Exception(_))
)
}

override def analyze(
ast: EOProg[EOExprOnly]
): F[OdinAnalysisResult] =
Expand Down Expand Up @@ -170,12 +166,33 @@ object EOOdinAnalyzer {

}

def analyzeSourceCode[EORepr, F[_]: Monad](
def directStateAccessAnalyzer[F[_]: MonadThrow]: ASTAnalyzer[F] =
new ASTAnalyzer[F] {

override val name: String = "Direct Access to Superclass State"

override def analyze(
ast: EOProg[EOExprOnly]
): F[OdinAnalysisResult] =
OdinAnalysisResult.fromThrow[F](name) {
for {
tmpTree <-
toThrow(Inliner.createObjectTree(ast))
tree <- toThrow(Inliner.resolveParents(tmpTree))
errors <-
toThrow(DetectStateAccess.analyze(tree))
} yield errors
}

}

def analyzeSourceCode[EORepr, F[_]](
analyzer: ASTAnalyzer[F]
)(
eoRepr: EORepr
)(implicit
parser: EoParser[EORepr, F, EOProg[EOExprOnly]]
m: Monad[F],
parser: EoParser[EORepr, F, EOProg[EOExprOnly]],
): F[OdinAnalysisResult] = for {
programAst <- parser.parse(eoRepr)
mutualRecursionErrors <-
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package org.polystat.odin.analysis.stateaccess

import cats.data.EitherNel
import higherkindness.droste.data.Fix
import org.polystat.odin.analysis.utils.Abstract
import org.polystat.odin.analysis.utils.inlining._
import org.polystat.odin.core.ast._
import org.polystat.odin.core.ast.astparams.EOExprOnly

import scala.annotation.tailrec

object DetectStateAccess {

type ObjInfo = ObjectInfo[ParentInfo[MethodInfo, ObjectInfo], MethodInfo]

case class State(
containerName: String,
statePath: List[String],
states: Vector[EONamedBnd]
)

case class StateChange(
method: EONamedBnd,
state: EONamedBnd,
statePath: List[String]
)

def collectNestedStates(mainParent: String)(
subTree: Inliner.CompleteObjectTree
): Vector[State] = {
val currentLvlStateNames = subTree
.info
.bnds
.collect {
case BndItself(
EOBndExpr(
bndName,
EOSimpleAppWithLocator("memory" | "cage", _)
)
) => bndName
}

Vector(
State(mainParent, subTree.info.fqn.names.tail, currentLvlStateNames)
) ++
subTree
.children
.flatMap(t => collectNestedStates(mainParent)(t._2))
}

def accumulateParentState(tree: Map[EONamedBnd, Inliner.CompleteObjectTree])(
currentParentLink: Option[ParentInfo[MethodInfo, ObjectInfo]],
existingStates: Vector[EONamedBnd] = Vector()
): Vector[State] = {
currentParentLink match {
case Some(parentLink) =>
val parentObj = parentLink.linkToParent.getOption(tree).get
val currentObjName = parentObj.info.name.name.name
val currentLvlStateNames = parentObj
.info
.bnds
.collect {
case BndItself(
EOBndExpr(
bndName,
EOSimpleAppWithLocator("memory" | "cage", _)
)
) if !existingStates.contains(bndName) =>
bndName
}
val currentLvlState =
State(currentObjName, List(), currentLvlStateNames)
val nestedStates = parentObj
.children
.flatMap(c =>
collectNestedStates(parentObj.info.name.name.name)(c._2)
)
.toVector

Vector(currentLvlState) ++ nestedStates ++
accumulateParentState(tree)(
parentObj.info.parentInfo,
existingStates ++ currentLvlStateNames
)

case None => Vector()
}
}

def getAccessedStates(method: (EONamedBnd, MethodInfo)): List[StateChange] = {
@tailrec
def hasSelfAsSource(dot: EODot[EOExprOnly]): Boolean = {
Fix.un(dot.src) match {
case EOSimpleAppWithLocator("self", x) if x == 0 => true
case innerDot @ EODot(_, _) => hasSelfAsSource(innerDot)
case _ => false
}
}

def buildDotChain(dot: EODot[EOExprOnly]): List[String] =
Fix.un(dot.src) match {
case EOSimpleAppWithLocator("self", x) if x == 0 => List()
case innerDot @ EODot(_, _) =>
buildDotChain(innerDot).appended(innerDot.name)
case _ => List()
}

val binds = method._2.body.bndAttrs

def processDot(
innerDot: EODot[Fix[EOExpr]],
state: String
): List[StateChange] = {
val stateName = EOAnyNameBnd(LazyName(state))
val containerChain = buildDotChain(innerDot)

List(StateChange(method._1, stateName, containerChain))
}

Abstract.foldAst[List[StateChange]](binds) {
case EOCopy(Fix(dot @ EODot(Fix(innerDot @ EODot(_, state)), _)), _)
if hasSelfAsSource(dot) =>
processDot(innerDot, state)

case dot @ EODot(_, state) if hasSelfAsSource(dot) =>
processDot(dot, state)
}
}

def detectStateAccesses(
tree: Map[EONamedBnd, Inliner.CompleteObjectTree]
)(obj: (EONamedBnd, Inliner.CompleteObjectTree)): List[String] = {
val availableParentStates =
accumulateParentState(tree)(obj._2.info.parentInfo)
val accessedStates = obj._2.info.methods.flatMap(getAccessedStates)
val results =
for {
StateChange(targetMethod, state, accessedStatePath) <- accessedStates
State(baseClass, statePath, changedStates) <- availableParentStates
} yield
if (changedStates.contains(state) && statePath == accessedStatePath) {
val objName = obj._2.info.fqn.names.toList.mkString(".")
val stateName = state.name.name
val method = targetMethod.name.name
val container = statePath.prepended(baseClass).mkString(".")

List(
f"Method '$method' of object '$objName' directly accesses state '$stateName' of base class '$container'"
)
} else List()

results.toList.flatten
}

def analyze[F[_]](
originalTree: Map[EONamedBnd, Inliner.CompleteObjectTree]
): EitherNel[String, List[String]] = {
def helper(
tree: Map[EONamedBnd, Inliner.CompleteObjectTree]
): List[String] =
tree
.filter(_._2.info.parentInfo.nonEmpty)
.flatMap(detectStateAccesses(originalTree))
.toList

def recurse(
tree: Map[EONamedBnd, Inliner.CompleteObjectTree]
): List[String] = {
val currentRes = helper(tree)
val children = tree.values.map(_.children)

currentRes ++ children.flatMap(recurse)
}

Right(recurse(originalTree))
}

}
Loading