All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.joern.jssrc2cpg.astcreation.AstCreatorHelper.scala Maven / Gradle / Ivy

The newest version!
package io.joern.jssrc2cpg.astcreation

import io.joern.jssrc2cpg.datastructures.*
import io.joern.jssrc2cpg.parser.BabelAst.*
import io.joern.jssrc2cpg.parser.BabelNodeInfo
import io.joern.x2cpg.frontendspecific.jssrc2cpg.Defines
import io.joern.x2cpg.utils.IntervalKeyPool
import io.joern.x2cpg.utils.NodeBuilders.{newClosureBindingNode, newLocalNode}
import io.joern.x2cpg.{Ast, ValidationMode}
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.codepropertygraph.generated.{EdgeTypes, EvaluationStrategies}
import io.shiftleft.codepropertygraph.generated.nodes.File.PropertyDefaults
import ujson.Value

import scala.collection.{mutable, SortedMap}
import scala.util.{Success, Try}

trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: AstCreator =>

  private val anonClassKeyPool = new IntervalKeyPool(first = 0, last = Long.MaxValue)

  protected def nextAnonClassName(): String = s"${anonClassKeyPool.next}"

  protected def createBabelNodeInfo(json: Value): BabelNodeInfo = {
    val c     = code(json)
    val ln    = line(json)
    val cn    = column(json)
    val lnEnd = lineEnd(json)
    val cnEnd = columnEnd(json)
    val node  = nodeType(json)
    BabelNodeInfo(node, json, c, ln, cn, lnEnd, cnEnd)
  }

  protected def notHandledYet(node: BabelNodeInfo): Ast = {
    val text =
      s"""Node type '${node.node}' not handled yet!
         |  Code: '${node.code}'
         |  File: '${parserResult.fullPath}'
         |  Line: ${node.lineNumber.getOrElse(-1)}
         |  Column: ${node.columnNumber.getOrElse(-1)}
         |  """.stripMargin
    logger.info(text)
    Ast(unknownNode(node, node.code))
  }

  protected def registerType(typeFullName: String): Unit = {
    global.usedTypes.putIfAbsent(typeFullName, true)
  }

  private def nodeType(node: Value): BabelNode = fromString(node("type").str)

  protected def codeForNodes(nodes: Seq[NewNode]): Option[String] = nodes.collectFirst {
    case id: NewIdentifier => id.name.replace("...", "")
    case clazz: NewTypeRef => clazz.code.stripPrefix("class ")
  }

  protected def nameForBabelNodeInfo(nodeInfo: BabelNodeInfo, defaultName: Option[String]): String = {
    defaultName
      .orElse(codeForBabelNodeInfo(nodeInfo).headOption)
      .getOrElse {
        val tmpName    = generateUnusedVariableName(usedVariableNames, "_tmp")
        val nLocalNode = localNode(nodeInfo, tmpName, tmpName, Defines.Any).order(0)
        diffGraph.addEdge(localAstParentStack.head, nLocalNode, EdgeTypes.AST)
        tmpName
      }
  }

  protected def generateUnusedVariableName(
    usedVariableNames: mutable.HashMap[String, Int],
    variableName: String
  ): String = {
    val counter             = usedVariableNames.get(variableName).map(_ + 1).getOrElse(0)
    val currentVariableName = s"${variableName}_$counter"
    usedVariableNames.put(variableName, counter)
    currentVariableName
  }

  protected def code(node: Value): String = {
    nodeOffsets(node) match {
      case Some((startOffset, endOffset)) =>
        shortenCode(parserResult.fileContent.substring(startOffset, endOffset).trim)
      case _ =>
        PropertyDefaults.Code
    }
  }

  protected def hasKey(node: Value, key: String): Boolean = Try(node(key)).isSuccess

  protected def safeStr(node: Value, key: String): Option[String] =
    if (hasKey(node, key)) Try(node(key).str).toOption else None

  protected def safeBool(node: Value, key: String): Option[Boolean] =
    if (hasKey(node, key)) Try(node(key).bool).toOption else None

  protected def safeObj(node: Value, key: String): Option[upickle.core.LinkedHashMap[String, Value]] = Try(
    node(key).obj
  ) match {
    case Success(value) if value.nonEmpty => Option(value)
    case _                                => None
  }

  protected def start(node: Value): Option[Int] = Try(node("start").num.toInt).toOption

  protected def end(node: Value): Option[Int] = Try(node("end").num.toInt).toOption

  protected def pos(node: Value): Option[Int] = Try(node("start").num.toInt).toOption

  protected def line(node: Value): Option[Int] = start(node).map(getLineOfSource)

  protected def lineEnd(node: Value): Option[Int] = end(node).map(getLineOfSource)

  protected def column(node: Value): Option[Int] = start(node).map(getColumnOfSource)

  protected def columnEnd(node: Value): Option[Int] = end(node).map(getColumnOfSource)

  // Returns the line number for a given position in the source.
  private def getLineOfSource(position: Int): Int = {
    val (_, lineNumber) = positionToLineNumberMapping.minAfter(position).get
    lineNumber
  }

  // Returns the column number for a given position in the source.
  private def getColumnOfSource(position: Int): Int = {
    val (_, firstPositionInLine) = positionToFirstPositionInLineMapping.minAfter(position).get
    position - firstPositionInLine
  }

  protected def positionLookupTables(source: String): (SortedMap[Int, Int], SortedMap[Int, Int]) = {
    val positionToLineNumber, positionToFirstPositionInLine = mutable.TreeMap.empty[Int, Int]
    val data                                                = source.toCharArray
    var lineNumber                                          = 1
    var firstPositionInLine                                 = 0
    var position                                            = 0
    while (position < data.length) {
      val isNewLine = data(position) == '\n'
      if (isNewLine) {
        positionToLineNumber.put(position, lineNumber)
        lineNumber += 1
        positionToFirstPositionInLine.put(position, firstPositionInLine)
        firstPositionInLine = position + 1
      }
      position += 1
    }
    positionToLineNumber.put(position, lineNumber)
    positionToFirstPositionInLine.put(position, firstPositionInLine)

    // for empty line at the end of each JS/TS file generated by BabelJsonParser:
    positionToLineNumber.put(position + 1, lineNumber + 1)
    positionToFirstPositionInLine.put(position + 1, 0)
    (positionToLineNumber, positionToFirstPositionInLine)
  }

  private def computeScopePath(stack: Option[ScopeElement]): String =
    new ScopeElementIterator(stack)
      .to(Seq)
      .reverse
      .collect { case methodScopeElement: MethodScopeElement => methodScopeElement.name }
      .mkString(":")

  private def isMethodOrGetSet(func: BabelNodeInfo): Boolean = {
    if (hasKey(func.json, "kind") && !func.json("kind").isNull) {
      val t = func.json("kind").str
      t == "method" || t == "get" || t == "set"
    } else false
  }

  private def calcMethodName(func: BabelNodeInfo): String = func.node match {
    case ObjectMethod if isMethodOrGetSet(func) && code(func.json("key")).startsWith("'") => nextClosureName()
    case TSCallSignatureDeclaration                                                       => nextClosureName()
    case TSConstructSignatureDeclaration => io.joern.x2cpg.Defines.ConstructorMethodName
    case _ if isMethodOrGetSet(func) =>
      if (hasKey(func.json("key"), "name")) func.json("key")("name").str
      else code(func.json("key"))
    case _ if safeStr(func.json, "kind").contains("constructor") => io.joern.x2cpg.Defines.ConstructorMethodName
    case _ if func.json("id").isNull                             => nextClosureName()
    case _                                                       => func.json("id")("name").str
  }

  protected def calcMethodNameAndFullName(func: BabelNodeInfo): (String, String) = {
    // functionNode.getName is not necessarily unique and thus the full name calculated based on the scope
    // is not necessarily unique. Specifically we have this problem with lambda functions which are defined
    // in the same scope.
    functionNodeToNameAndFullName.get(func) match {
      case Some(nameAndFullName) => nameAndFullName
      case None =>
        val intendedName   = calcMethodName(func)
        val fullNamePrefix = s"${parserResult.filename}:${computeScopePath(scope.getScopeHead)}:"
        var name           = intendedName
        var fullName       = ""
        var isUnique       = false
        var i              = 1
        while (!isUnique) {
          fullName = s"$fullNamePrefix$name"
          if (functionFullNames.contains(fullName)) {
            name = s"$intendedName$i"
            i += 1
          } else {
            isUnique = true
          }
        }
        functionFullNames.add(fullName)
        functionNodeToNameAndFullName(func) = (name, fullName)
        (name, fullName)
    }
  }

  protected def stripQuotes(str: String): String = str
    .stripPrefix("\"")
    .stripSuffix("\"")
    .stripPrefix("'")
    .stripSuffix("'")
    .stripPrefix("`")
    .stripSuffix("`")

  /** In JS it is possible to create anonymous classes. We have to handle this here.
    */
  private def calcTypeName(classNode: BabelNodeInfo): String =
    if (hasKey(classNode.json, "id") && !classNode.json("id").isNull) code(classNode.json("id"))
    else nextAnonClassName()

  protected def calcTypeNameAndFullName(
    classNode: BabelNodeInfo,
    preCalculatedName: Option[String] = None
  ): (String, String) = {
    val name           = preCalculatedName.getOrElse(calcTypeName(classNode))
    val fullNamePrefix = s"${parserResult.filename}:${computeScopePath(scope.getScopeHead)}:"
    val fullName       = s"$fullNamePrefix$name"
    (name, fullName)
  }

  protected def createVariableReferenceLinks(): Unit = {
    val resolvedReferenceIt = scope.resolve(createMethodLocalForUnresolvedReference)
    val capturedLocals      = mutable.HashMap.empty[String, NewNode]

    resolvedReferenceIt.foreach { case ResolvedReference(variableNodeId, origin) =>
      var currentScope           = origin.stack
      var currentReference       = origin.referenceNode
      var nextReference: NewNode = null

      var done = false
      while (!done) {
        val localOrCapturedLocalNodeOption =
          if (currentScope.get.nameToVariableNode.contains(origin.variableName)) {
            done = true
            Option(variableNodeId)
          } else {
            currentScope.flatMap {
              case methodScope: MethodScopeElement
                  if methodScope.scopeNode.isInstanceOf[NewTypeDecl] || methodScope.scopeNode
                    .isInstanceOf[NewNamespaceBlock] =>
                currentScope = Option(Scope.getEnclosingMethodScopeElement(currentScope))
                None
              case methodScope: MethodScopeElement =>
                // We have reached a MethodScope and still did not find a local variable to link to.
                // For all non local references the CPG format does not allow us to link
                // directly. Instead we need to create a fake local variable in method
                // scope and link to this local which itself carries the information
                // that it is a captured variable. This needs to be done for each
                // method scope until we reach the originating scope.
                val closureBindingIdProperty = s"${methodScope.methodFullName}:${origin.variableName}"
                capturedLocals.updateWith(closureBindingIdProperty) {
                  case None =>
                    val methodScopeNode = methodScope.scopeNode
                    val localNode =
                      newLocalNode(origin.variableName, Defines.Any, Option(closureBindingIdProperty)).order(0)
                    diffGraph.addEdge(methodScopeNode, localNode, EdgeTypes.AST)
                    val closureBindingNode = newClosureBindingNode(
                      closureBindingIdProperty,
                      origin.variableName,
                      EvaluationStrategies.BY_REFERENCE
                    )
                    methodScope.capturingRefId.foreach(ref =>
                      diffGraph.addEdge(ref, closureBindingNode, EdgeTypes.CAPTURE)
                    )
                    nextReference = closureBindingNode
                    Option(localNode)
                  case someLocalNode =>
                    // When there is already a LOCAL representing the capturing, we do not
                    // need to process the surrounding scope element as this has already
                    // been processed.
                    done = true
                    someLocalNode
                }
              case _: BlockScopeElement => None
            }
          }

        localOrCapturedLocalNodeOption.foreach { localOrCapturedLocalNode =>
          diffGraph.addEdge(currentReference, localOrCapturedLocalNode, EdgeTypes.REF)
          currentReference = nextReference
        }
        currentScope = currentScope.get.surroundingScope
      }
    }
  }

  private def createMethodLocalForUnresolvedReference(
    methodScopeNodeId: NewNode,
    variableName: String
  ): (NewNode, ScopeType) = {
    val local = newLocalNode(variableName, Defines.Any).order(0)
    diffGraph.addEdge(methodScopeNodeId, local, EdgeTypes.AST)
    (local, MethodScope)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy