diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala index cd9c54243d56..3695e1ff46b6 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/AstCreator.scala @@ -154,6 +154,10 @@ class AstCreator( TypeConstants.Any } + private[astcreation] def isResolvedTypeFullName(typeFullName: String): Boolean = { + typeFullName != TypeConstants.Any && !typeFullName.startsWith(Defines.UnresolvedNamespace) + } + /** Custom printer that omits comments. To be used by [[code]] */ private val codePrinterOptions = new DefaultPrinterConfiguration() .removeOption(new DefaultConfigurationOption(ConfigOption.PRINT_COMMENTS)) @@ -372,8 +376,10 @@ class AstCreator( case _ => None } - def argumentTypesForMethodLike(maybeResolvedMethodLike: Try[ResolvedMethodLikeDeclaration]): Option[List[String]] = { - maybeResolvedMethodLike.toOption + def argumentTypesForMethodLike( + maybeResolvedMethodLike: Option[ResolvedMethodLikeDeclaration] + ): Option[List[String]] = { + maybeResolvedMethodLike .flatMap(calcParameterTypes(_, ResolvedTypeParametersMap.empty())) } diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala index 5b8d8dea3ad2..79d79b8f5f06 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForMethodsCreator.scala @@ -4,6 +4,7 @@ import io.joern.x2cpg.utils.AstPropertiesUtil.* import com.github.javaparser.ast.NodeList import com.github.javaparser.ast.body.{ CallableDeclaration, + CompactConstructorDeclaration, ConstructorDeclaration, FieldDeclaration, MethodDeclaration, @@ -11,7 +12,11 @@ import com.github.javaparser.ast.body.{ VariableDeclarator } import com.github.javaparser.ast.stmt.{BlockStmt, ExplicitConstructorInvocationStmt} -import com.github.javaparser.resolution.declarations.{ResolvedMethodDeclaration, ResolvedMethodLikeDeclaration} +import com.github.javaparser.resolution.declarations.{ + ResolvedMethodDeclaration, + ResolvedMethodLikeDeclaration, + ResolvedParameterDeclaration +} import com.github.javaparser.resolution.types.ResolvedType import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParametersMap import io.joern.javasrc2cpg.astcreation.{AstCreator, ExpectedType} @@ -21,24 +26,30 @@ import io.joern.x2cpg.utils.NodeBuilders import io.joern.x2cpg.utils.NodeBuilders.* import io.joern.x2cpg.{Ast, Defines} import io.shiftleft.codepropertygraph.generated.nodes.{ + AstNodeNew, NewBlock, + NewCall, + NewFieldIdentifier, NewIdentifier, NewMethod, NewMethodParameterIn, NewMethodReturn, NewModifier } -import io.shiftleft.codepropertygraph.generated.{EvaluationStrategies, ModifierTypes} +import io.shiftleft.codepropertygraph.generated.{ + DispatchTypes, + EdgeTypes, + EvaluationStrategies, + ModifierTypes, + NodeTypes, + Operators, + nodes +} import io.joern.javasrc2cpg.scope.JavaScopeElement.fullName import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.RichOptional import scala.util.{Failure, Success, Try} -import io.shiftleft.codepropertygraph.generated.nodes.AstNodeNew -import io.shiftleft.codepropertygraph.generated.nodes.NewCall -import io.shiftleft.codepropertygraph.generated.Operators -import io.shiftleft.codepropertygraph.generated.DispatchTypes -import io.shiftleft.codepropertygraph.generated.EdgeTypes import com.github.javaparser.ast.Node import com.github.javaparser.ast.`type`.ClassOrInterfaceType import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserParameterDeclaration @@ -52,7 +63,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val typeParameters = getIdentifiersForTypeParameters(methodDeclaration) methodDeclaration.getType - val maybeResolved = tryWithSafeStackOverflow(methodDeclaration.resolve()) + val maybeResolved = tryWithSafeStackOverflow(methodDeclaration.resolve()).toOption val expectedReturnType = tryWithSafeStackOverflow( symbolSolver.toResolvedType(methodDeclaration.getType, classOf[ResolvedType]) ).toOption @@ -73,7 +84,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => ) typeParameters.foreach { typeParameter => scope.addTopLevelType(typeParameter.name, typeParameter.typeFullName) } - val parameterAsts = astsForParameterList(methodDeclaration.getParameters) + val parameterAsts = astsForParameterList(methodDeclaration.getParameters.asScala.toList) val parameterTypes = argumentTypesForMethodLike(maybeResolved) val signature = composeSignature(returnTypeFullName, parameterTypes, parameterAsts.size) val namespaceName = scope.enclosingTypeDecl.fullName.getOrElse(Defines.UnresolvedNamespace) @@ -85,7 +96,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => val thisNode = Option.when(!methodDeclaration.isStatic) { val typeFullName = scope.enclosingTypeDecl.fullName - thisNodeForMethod(typeFullName, line(methodDeclaration)) + thisNodeForMethod(typeFullName, line(methodDeclaration), column(methodDeclaration)) } val thisAst = thisNode.map(Ast(_)).toList @@ -117,6 +128,59 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => methodAstWithAnnotations(methodNode, thisAst ++ parameterAsts, bodyAst, methodReturn, modifiers, annotationAsts) } + private[declarations] def astForRecordParameterAccessor( + parameter: Parameter, + recordTypeFullName: String, + parameterName: String, + parameterTypeFullName: String + ): Ast = { + val signature = + if (isResolvedTypeFullName(parameterTypeFullName)) + composeSignature(Option(parameterTypeFullName), Option(Nil), 0) + else + composeSignature(None, Option(Nil), 0) + + val methodFullName = composeMethodFullName(recordTypeFullName, parameterName, signature) + + val methodReturn = + newMethodReturnNode(parameterTypeFullName, line = line(parameter), column = column(parameter)) + + val methodRoot = methodNode( + parameter, + parameterName, + s"public ${code(parameter.getType)} ${parameterName}()", + methodFullName, + Option(signature), + filename, + Option(NodeTypes.TYPE_DECL), + Option(recordTypeFullName) + ) + + val modifier = newModifierNode(ModifierTypes.PUBLIC) + + val thisParameter = thisNodeForMethod(Option(recordTypeFullName), line(parameter), column(parameter)) + + val thisIdentifier = identifierNode(parameter, thisParameter.name, thisParameter.code, recordTypeFullName) + val thisIdentifierAst = Ast(thisIdentifier).withRefEdge(thisIdentifier, thisParameter) + val fieldIdentifier = fieldIdentifierNode(parameter, parameterName, parameterName) + + val fieldAccessNode = newOperatorCallNode( + Operators.fieldAccess, + s"${thisIdentifier.code}.${fieldIdentifier.code}", + Option(parameterTypeFullName), + line(parameter), + column(parameter) + ) + val fieldAccessCall = callAst(fieldAccessNode, thisIdentifierAst :: Ast(fieldIdentifier) :: Nil) + + val returnStmt = returnNode(parameter, s"return ${fieldAccessNode.code}") + val returnAst = Ast(returnStmt).withChild(fieldAccessCall) + + val methodBodyAst = blockAst(blockNode(parameter), returnAst :: Nil) + + methodAst(methodRoot, Ast(thisParameter) :: Nil, methodBodyAst, methodReturn, modifier :: Nil) + } + private def abstractModifierForCallable( callableDeclaration: CallableDeclaration[?], isInterfaceMethod: Boolean @@ -131,13 +195,23 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => } } - private def modifiersForMethod(methodDeclaration: CallableDeclaration[?]): List[NewModifier] = { + private def modifiersForMethod( + methodDeclaration: CallableDeclaration[?] | CompactConstructorDeclaration + ): List[NewModifier] = { val isInterfaceMethod = scope.enclosingTypeDecl.isInterface - val abstractModifier = abstractModifierForCallable(methodDeclaration, isInterfaceMethod) + val abstractModifier = Option + .when(methodDeclaration.isCallableDeclaration)( + abstractModifierForCallable(methodDeclaration.asCallableDeclaration(), isInterfaceMethod) + ) + .flatten - val staticVirtualModifierType = if (methodDeclaration.isStatic) ModifierTypes.STATIC else ModifierTypes.VIRTUAL - val staticVirtualModifier = Some(newModifierNode(staticVirtualModifierType)) + // TODO: The opposite of static is not virtual + val staticVirtualModifierType = + if (methodDeclaration.isCallableDeclaration && methodDeclaration.asCallableDeclaration().isStatic) + ModifierTypes.STATIC + else ModifierTypes.VIRTUAL + val staticVirtualModifier = Some(newModifierNode(staticVirtualModifierType)) val accessModifierType = if (methodDeclaration.isPublic) { Some(ModifierTypes.PUBLIC) @@ -190,25 +264,38 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => } def astForDefaultConstructor(originNode: Node, instanceFieldDeclarations: List[FieldDeclaration]): Ast = { + val constructorNode = NewMethod() + .name(io.joern.x2cpg.Defines.ConstructorMethodName) + .filename(filename) + .isExternal(false) + scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) + + val parameters = scope.enclosingTypeDecl.get.recordParameters + val parameterAsts = parameters.zipWithIndex.map { case (param, idx) => + astForParameter(param, idx + 1) + } + val parameterTypes = parameterAsts.map(_.rootType.getOrElse(defaultTypeFallback())) + val resolvedParameterTypes = Option.when(parameterTypes.forall(isResolvedTypeFullName))(parameterTypes) + val typeFullName = scope.enclosingTypeDecl.fullName - val signature = s"${TypeConstants.Void}()" + val signature = composeSignature(Option(TypeConstants.Void), resolvedParameterTypes, parameterAsts.size) val fullName = composeMethodFullName( typeFullName.getOrElse(Defines.UnresolvedNamespace), Defines.ConstructorMethodName, signature ) - val constructorNode = NewMethod() - .name(io.joern.x2cpg.Defines.ConstructorMethodName) - .fullName(fullName) - .signature(signature) - .filename(filename) - .isExternal(false) - scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) + constructorNode.fullName(fullName) + constructorNode.signature(signature) - val thisNode = thisNodeForMethod(typeFullName, lineNumber = None) + val thisNode = thisNodeForMethod(typeFullName, lineNumber = None, columnNumber = None) scope.enclosingMethod.foreach(_.addParameter(thisNode)) - val bodyStatementAsts = astsForFieldInitializers(instanceFieldDeclarations) + val recordParameterAssignments = parameterAsts + .flatMap(_.nodes) + .collect { case param: nodes.NewMethodParameterIn => param } + .map(astForEponymousFieldAssignment(thisNode, _)) + val bodyStatementAsts = + astsForFieldInitializers(instanceFieldDeclarations) ++ recordParameterAssignments val temporaryLocalAsts = scope.enclosingMethod.map(_.getTemporaryLocals).getOrElse(Nil).map(Ast(_)) val returnNode = newMethodReturnNode(TypeConstants.Void, line = None, column = None) @@ -219,7 +306,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => originNode, constructorNode, thisNode, - explicitParameterAsts = Nil, + explicitParameterAsts = parameterAsts, bodyStatementAsts = temporaryLocalAsts ++ bodyStatementAsts, methodReturn = returnNode, annotationAsts = Nil, @@ -232,6 +319,52 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => constructorAst } + private def astForEponymousFieldAssignment( + thisParam: NewMethodParameterIn, + recordParameter: NewMethodParameterIn + ): Ast = { + val thisIdentifier = NewIdentifier() + .name(thisParam.name) + .code(thisParam.name) + .typeFullName(thisParam.typeFullName) + .lineNumber(recordParameter.lineNumber) + .columnNumber(recordParameter.columnNumber) + .dynamicTypeHintFullName(thisParam.dynamicTypeHintFullName) + val thisIdentifierAst = Ast(thisIdentifier).withRefEdge(thisIdentifier, thisParam) + + val fieldIdentifier = NewFieldIdentifier() + .canonicalName(recordParameter.name) + .code(recordParameter.name) + + val fieldAccessNode = newOperatorCallNode( + Operators.fieldAccess, + s"${thisIdentifier.code}.${fieldIdentifier.code}", + Option(recordParameter.typeFullName), + recordParameter.lineNumber, + recordParameter.columnNumber + ) + val fieldAccessAst = callAst(fieldAccessNode, thisIdentifierAst :: Ast(fieldIdentifier) :: Nil) + + val recordParamIdentifier = NewIdentifier() + .name(recordParameter.name) + .code(recordParameter.name) + .typeFullName(recordParameter.typeFullName) + .lineNumber(recordParameter.lineNumber) + .columnNumber(recordParameter.columnNumber) + .dynamicTypeHintFullName(recordParameter.dynamicTypeHintFullName) + val recordParamIdentifierAst = Ast(recordParamIdentifier).withRefEdge(recordParamIdentifier, recordParameter) + + val assignmentNode = newOperatorCallNode( + Operators.assignment, + s"${fieldAccessNode.code} = ${recordParamIdentifier.code}", + Option(recordParameter.typeFullName), + recordParameter.lineNumber, + recordParameter.columnNumber + ) + + callAst(assignmentNode, fieldAccessAst :: recordParamIdentifierAst :: Nil) + } + private def astForParameter(parameter: Parameter, childNum: Int): Ast = { val maybeArraySuffix = if (parameter.isVarArgs) "[]" else "" val rawParameterTypeName = @@ -268,23 +401,29 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => methodLike: ResolvedMethodLikeDeclaration, typeParamValues: ResolvedTypeParametersMap ): Option[List[String]] = { - val parameterTypes = - Range(0, methodLike.getNumberOfParams) - .flatMap { index => - Try(methodLike.getParam(index)).toOption - } - .map { param => - tryWithSafeStackOverflow(param.getType).toOption - .flatMap(paramType => typeInfoCalc.fullName(paramType, typeParamValues)) - // In a scenario where we have an import of an external type e.g. `import foo.bar.Baz` and - // this parameter's type is e.g. `Baz`, the lookup will fail. However, if we lookup - // for `Baz` instead (i.e. without type arguments), then the lookup will succeed. - .orElse( - Try( - param.asInstanceOf[JavaParserParameterDeclaration].getWrappedNode.getType.asClassOrInterfaceType - ).toOption.flatMap(t => scope.lookupType(t.getNameAsString)) - ) - } + val parameters = + Range(0, methodLike.getNumberOfParams).flatMap { index => + Try(methodLike.getParam(index)).toOption + }.toList + + calcParameterTypes(parameters, typeParamValues) + } + + def calcParameterTypes( + parameters: List[ResolvedParameterDeclaration], + typeParamValues: ResolvedTypeParametersMap + ): Option[List[String]] = { + val parameterTypes = parameters.map { param => + tryWithSafeStackOverflow(param.getType).toOption + .flatMap(paramType => typeInfoCalc.fullName(paramType, typeParamValues)) + // In a scenario where we have an import of an external type e.g. `import foo.bar.Baz` and + // this parameter's type is e.g. `Baz`, the lookup will fail. However, if we lookup + // for `Baz` instead (i.e. without type arguments), then the lookup will succeed. + .orElse( + Try(param.asInstanceOf[JavaParserParameterDeclaration].getWrappedNode.getType.asClassOrInterfaceType).toOption + .flatMap(t => scope.lookupType(t.getNameAsString)) + ) + } toOptionList(parameterTypes) } @@ -312,14 +451,15 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => composeSignature(maybeReturnType, maybeParameterTypes, method.getNumberOfParams) } - private def astsForParameterList(parameters: NodeList[Parameter]): Seq[Ast] = { - parameters.asScala.toList.zipWithIndex.map { case (param, idx) => + + private def astsForParameterList(parameters: List[Parameter]): Seq[Ast] = { + parameters.zipWithIndex.map { case (param, idx) => astForParameter(param, idx + 1) } } private def partialConstructorAsts( - constructorDeclarations: List[ConstructorDeclaration], + constructorDeclarations: List[ConstructorDeclaration | CompactConstructorDeclaration], instanceFieldDeclarations: List[FieldDeclaration] ): List[PartialConstructorDeclaration] = { constructorDeclarations.map { constructorDeclaration => @@ -327,12 +467,25 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => .name(io.joern.x2cpg.Defines.ConstructorMethodName) scope.pushMethodScope(constructorNode, ExpectedType.Void, isStatic = false) - val maybeResolved = tryWithSafeStackOverflow(constructorDeclaration.resolve()) + val maybeResolved = Option + .when(constructorDeclaration.isConstructorDeclaration)( + tryWithSafeStackOverflow(constructorDeclaration.resolve()).toOption + ) + .flatten - val parameterAsts = astsForParameterList(constructorDeclaration.getParameters).toList - val paramTypes = argumentTypesForMethodLike(maybeResolved) - val signature = composeSignature(Some(TypeConstants.Void), paramTypes, parameterAsts.size) - val typeFullName = scope.enclosingTypeDecl.fullName + val parameters = constructorDeclaration match { + case regularConstructor: ConstructorDeclaration => regularConstructor.getParameters.asScala.toList + case compactConstructor: CompactConstructorDeclaration => scope.enclosingTypeDecl.get.recordParameters + } + val parameterAsts = astsForParameterList(parameters).toList + val paramTypes = constructorDeclaration match { + case constructor: ConstructorDeclaration => argumentTypesForMethodLike(maybeResolved) + case constructor: CompactConstructorDeclaration => + val resolvedParams = parameters.flatMap(param => tryWithSafeStackOverflow(param.resolve()).toOption).toList + calcParameterTypes(resolvedParams, ResolvedTypeParametersMap.empty()) + } + val signature = composeSignature(Some(TypeConstants.Void), paramTypes, parameterAsts.size) + val typeFullName = scope.enclosingTypeDecl.fullName val fullName = composeMethodFullName( typeFullName.getOrElse(Defines.UnresolvedNamespace), @@ -351,10 +504,19 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => } } - val thisNode = thisNodeForMethod(typeFullName, line(constructorDeclaration)) + val thisNode = thisNodeForMethod(typeFullName, line(constructorDeclaration), column(constructorDeclaration)) scope.enclosingMethod.get.addParameter(thisNode) scope.pushBlockScope() + val recordParameterAssignments = constructorDeclaration match { + case constructor: CompactConstructorDeclaration => + parameterAsts + .flatMap(_.nodes) + .collect { case param: nodes.NewMethodParameterIn => param } + .map(astForEponymousFieldAssignment(thisNode, _)) + case _ => Nil + } + val bodyStatements = constructorDeclaration.getBody.getStatements.asScala.toList val statementsAsts = bodyStatements.flatMap(astsForStatement) val bodyContainsThis = bodyStatements.headOption @@ -370,7 +532,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => // The this(...) call must always be the first statement in the body, but adding the fieldAssignmentsAndTempLocals // before the body asts here is safe, since the list will be empty if the body does start with this() - val bodyAsts = fieldAssignmentsAndTempLocals ++ statementsAsts + val bodyAsts = recordParameterAssignments ++ fieldAssignmentsAndTempLocals ++ statementsAsts scope.popBlockScope() val methodReturn = constructorReturnNode(constructorDeclaration) @@ -467,7 +629,7 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => } def astsForConstructors( - constructorDeclarations: List[ConstructorDeclaration], + constructorDeclarations: List[ConstructorDeclaration | CompactConstructorDeclaration], instanceFieldDeclarations: List[FieldDeclaration] ): Map[Node, Ast] = { val partialConstructors = partialConstructorAsts(constructorDeclarations, instanceFieldDeclarations) @@ -476,7 +638,9 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => }.toMap } - private def constructorReturnNode(constructorDeclaration: ConstructorDeclaration): NewMethodReturn = { + private def constructorReturnNode( + constructorDeclaration: ConstructorDeclaration | CompactConstructorDeclaration + ): NewMethodReturn = { val line = constructorDeclaration.getEnd.map(_.line).toScala val column = constructorDeclaration.getEnd.map(_.column).toScala newMethodReturnNode(TypeConstants.Void, None, line, column) @@ -503,22 +667,30 @@ private[declarations] trait AstForMethodsCreator { this: AstCreator => /** Constructor and Method declarations share a lot of fields, so this method adds the fields they have in common. * `fullName` and `signature` are omitted */ - private def createPartialMethod(declaration: CallableDeclaration[?]): NewMethod = { - val code = declaration.getDeclarationAsString.trim + private def createPartialMethod(declaration: CallableDeclaration[?] | CompactConstructorDeclaration): NewMethod = { + val methodCode = declaration match { + case callableDeclaration: CallableDeclaration[?] => callableDeclaration.getDeclarationAsString.trim + case compactConstructor: CompactConstructorDeclaration => code(compactConstructor) + } val columnNumber = declaration.getBegin.map(x => Integer.valueOf(x.column)).toScala val endLine = declaration.getEnd.map(x => Integer.valueOf(x.line)).toScala val endColumn = declaration.getEnd.map(x => Integer.valueOf(x.column)).toScala val placeholderFullName = "" - methodNode(declaration, declaration.getNameAsString(), code, placeholderFullName, None, filename) + methodNode(declaration, declaration.getNameAsString(), methodCode, placeholderFullName, None, filename) } - def thisNodeForMethod(maybeTypeFullName: Option[String], lineNumber: Option[Int]): NewMethodParameterIn = { + def thisNodeForMethod( + maybeTypeFullName: Option[String], + lineNumber: Option[Int], + columnNumber: Option[Int] + ): NewMethodParameterIn = { val typeFullName = typeInfoCalc.registerType(maybeTypeFullName.getOrElse(defaultTypeFallback())) NodeBuilders.newThisParameterNode( typeFullName = typeFullName, dynamicTypeHintFullName = maybeTypeFullName.toSeq, - line = lineNumber + line = lineNumber, + column = columnNumber ) } } diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala index 53119ad19c5f..b74c873d3572 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/declarations/AstForTypeDeclsCreator.scala @@ -2,13 +2,17 @@ package io.joern.javasrc2cpg.astcreation.declarations import com.github.javaparser.ast.body.{ AnnotationDeclaration, + AnnotationMemberDeclaration, BodyDeclaration, ClassOrInterfaceDeclaration, + CompactConstructorDeclaration, ConstructorDeclaration, EnumConstantDeclaration, + EnumDeclaration, FieldDeclaration, InitializerDeclaration, MethodDeclaration, + RecordDeclaration, TypeDeclaration, VariableDeclarator } @@ -57,9 +61,6 @@ import scala.jdk.CollectionConverters.* import scala.util.{Success, Try} import com.github.javaparser.ast.expr.ObjectCreationExpr import com.github.javaparser.ast.stmt.LocalClassDeclarationStmt -import com.github.javaparser.ast.body.AnnotationMemberDeclaration -import com.github.javaparser.ast.body.CompactConstructorDeclaration -import com.github.javaparser.ast.body.EnumDeclaration import io.joern.javasrc2cpg.scope.Scope.ScopeVariable import com.github.javaparser.ast.Node import com.github.javaparser.resolution.types.ResolvedReferenceType @@ -117,7 +118,7 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => methodDeclaration.getNameAsString }.toSet - scope.pushTypeDeclScope(typeDeclRoot, scope.isEnclosingScopeStatic, declaredMethodNames) + scope.pushTypeDeclScope(typeDeclRoot, scope.isEnclosingScopeStatic, declaredMethodNames, Nil) val memberAsts = astsForTypeDeclMembers(expr, body, isInterface = false, typeFullName) val localDecls = scope.localDeclsInScope @@ -173,7 +174,16 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => createTypeDeclNode(typeDeclaration, astParentType, astParentFullName, isInterface, fullNameOverride) val declaredMethodNames = typeDeclaration.getMethods.asScala.map(_.getNameAsString).toSet - scope.pushTypeDeclScope(typeDeclRoot, typeDeclaration.isStatic, declaredMethodNames) + + val (recordParameters, recordParameterAsts) = typeDeclaration match { + case recordDeclaration: RecordDeclaration => + val parameters = recordDeclaration.getParameters.asScala.toList + val asts = astsForRecordParameters(recordDeclaration, typeDeclRoot.fullName) + (parameters, asts) + case _ => (Nil, Nil) + } + + scope.pushTypeDeclScope(typeDeclRoot, typeDeclaration.isStatic, declaredMethodNames, recordParameters) addTypeDeclTypeParamsToScope(typeDeclaration) val annotationAsts = typeDeclaration.getAnnotations.asScala.map(astForAnnotationExpr) @@ -182,6 +192,7 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => case enumDeclaration: EnumDeclaration => enumDeclaration.getEntries.asScala.toList case _ => Nil } + val memberAsts = astsForTypeDeclMembers( typeDeclaration, @@ -194,6 +205,7 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => val lambdaMethods = scope.lambdaMethodsInScope val typeDeclAst = Ast(typeDeclRoot) + .withChildren(recordParameterAsts) .withChildren(memberAsts) .withChildren(annotationAsts) .withChildren(localDecls) @@ -228,6 +240,32 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => typeDeclAst } + private def astsForRecordParameters(recordDeclaration: RecordDeclaration, recordTypeFullName: String): List[Ast] = { + val explicitMethodNames = recordDeclaration.getMethods.asScala.map(_.getNameAsString).toSet + + recordDeclaration.getParameters.asScala.toList.flatMap { parameter => + val parameterName = parameter.getNameAsString + val parameterTypeFullName = tryWithSafeStackOverflow { + val typ = parameter.getType + scope + .lookupScopeType(typ.asString()) + .map(_.typeFullName) + .orElse(typeInfoCalc.fullName(typ)) + .getOrElse(defaultTypeFallback(typ)) + }.toOption.getOrElse(defaultTypeFallback()) + + val parameterMember = memberNode(parameter, parameterName, code(parameter), parameterTypeFullName) + val privateModifier = newModifierNode(ModifierTypes.PRIVATE) + val memberAst = Ast(parameterMember).withChild(Ast(privateModifier)) + + val accessorMethodAst = Option.unless(explicitMethodNames.contains(parameterName))( + astForRecordParameterAccessor(parameter, recordTypeFullName, parameterName, parameterTypeFullName) + ) + + memberAst :: accessorMethodAst.toList + } + } + private def bindingTypeForReferenceType(typ: ResolvedReferenceType): Option[JavaparserBindingDeclType] = { typ.getTypeDeclaration.toScala.map(typeDecl => scope.getDeclBinding(typeDecl.getName) match { @@ -343,19 +381,35 @@ private[declarations] trait AstForTypeDeclsCreator { this: AstCreator => } val constructorAstMap = astsForConstructors( - members.collect { case constructor: ConstructorDeclaration => - constructor + members.collect { + case constructor: ConstructorDeclaration => constructor + case constructor: CompactConstructorDeclaration => constructor }, instanceFields ) val membersAsts = membersAstPairs.flatMap { - case (constructor: ConstructorDeclaration, _) => - constructorAstMap.get(constructor) - case (_, asts) => asts + case (constructor: ConstructorDeclaration, _) => constructorAstMap.get(constructor) + case (constructor: CompactConstructorDeclaration, _) => constructorAstMap.get(constructor) + case (_, asts) => asts } - val defaultConstructorAst = Option.when(!(isInterface || members.exists(_.isInstanceOf[ConstructorDeclaration]))) { + val hasCanonicalConstructor = scope.enclosingTypeDecl.get.recordParameters match { + case Nil => members.exists(member => member.isConstructorDeclaration || member.isCompactConstructorDeclaration) + + case recordParameters => + members.collect { + case compactConstructorDeclaration: CompactConstructorDeclaration => compactConstructorDeclaration + + case constructorDeclaration: ConstructorDeclaration + if constructorDeclaration.getParameters.asScala + .map(_.getType) + .toList + .equals(recordParameters.map(_.getType)) => + constructorDeclaration + }.nonEmpty + } + val defaultConstructorAst = Option.when(!(isInterface || hasCanonicalConstructor)) { astForDefaultConstructor(originNode, instanceFields) } diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala index b4f2348ba8a7..d4a8887e371b 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForCallExpressionsCreator.scala @@ -59,7 +59,7 @@ trait AstForCallExpressionsCreator { this: AstCreator => val expressionTypeFullName = expressionReturnTypeFullName(call).orElse(getTypeFullName(expectedReturnType)).map(typeInfoCalc.registerType) - val argumentTypes = argumentTypesForMethodLike(maybeResolvedCall) + val argumentTypes = argumentTypesForMethodLike(maybeResolvedCall.toOption) val returnType = maybeResolvedCall .map { resolvedCall => typeInfoCalc.fullName(resolvedCall.getReturnType, ResolvedTypeParametersMap.empty()) @@ -232,7 +232,7 @@ trait AstForCallExpressionsCreator { this: AstCreator => scope.addLocalDecl(anonymousClassDecl) } - val argumentTypes = argumentTypesForMethodLike(maybeResolvedExpr) + val argumentTypes = argumentTypesForMethodLike(maybeResolvedExpr.toOption) val allocNode = newOperatorCallNode( Operators.alloc, diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala index 29b3b8aea119..3afc16003313 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForLambdasCreator.scala @@ -71,7 +71,7 @@ private[expressions] trait AstForLambdasCreator { this: AstCreator => .find { identifier => identifier.name == NameConstants.This || identifier.name == NameConstants.Super } .map { _ => val typeFullName = scope.enclosingTypeDecl.fullName - Ast(thisNodeForMethod(typeFullName, line(expr))) + Ast(thisNodeForMethod(typeFullName, line(expr), column(expr))) } .toList diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForSimpleExpressionsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForSimpleExpressionsCreator.scala index b34089a01bd0..eb20a61220c2 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForSimpleExpressionsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/expressions/AstForSimpleExpressionsCreator.scala @@ -459,7 +459,7 @@ trait AstForSimpleExpressionsCreator { this: AstCreator => case Success(resolvedMethod) => val returnType = tryWithSafeStackOverflow(resolvedMethod.getReturnType).toOption.flatMap(typeInfoCalc.fullName) - val parameterTypes = argumentTypesForMethodLike(Success(resolvedMethod)) + val parameterTypes = argumentTypesForMethodLike(Option(resolvedMethod)) composeSignature(returnType, parameterTypes, resolvedMethod.getNumberOfParams) } diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForSimpleStatementsCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForSimpleStatementsCreator.scala index af4c1b8e4e1f..cf818eef7484 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForSimpleStatementsCreator.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/astcreation/statements/AstForSimpleStatementsCreator.scala @@ -100,7 +100,7 @@ trait AstForSimpleStatementsCreator { this: AstCreator => // TODO Handle super val maybeResolved = tryWithSafeStackOverflow(stmt.resolve()) val args = argAstsForCall(stmt, maybeResolved, stmt.getArguments) - val argTypes = argumentTypesForMethodLike(maybeResolved) + val argTypes = argumentTypesForMethodLike(maybeResolved.toOption) // TODO: We can do better than defaultTypeFallback() for the fallback type by looking at the enclosing // type decl name or `extends X` name for `this` and `super` calls respectively. diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala index ff9ba83fe718..448a13ea9116 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/JavaScopeElement.scala @@ -1,5 +1,6 @@ package io.joern.javasrc2cpg.scope +import com.github.javaparser.ast.body.Parameter import com.github.javaparser.ast.expr.TypePatternExpr import io.joern.javasrc2cpg.scope.Scope.* import io.joern.javasrc2cpg.scope.JavaScopeElement.* @@ -175,7 +176,8 @@ object JavaScopeElement { override val isStatic: Boolean, private[scope] val capturedVariables: Map[String, CapturedVariable], outerClassType: Option[String], - val declaredMethodNames: Set[String] + val declaredMethodNames: Set[String], + val recordParameters: List[Parameter] )(implicit disableTypeFallback: Boolean) extends JavaScopeElement(disableTypeFallback) with TypeDeclContainer diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala index bc747b6dd25e..6f26c9e46498 100644 --- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala +++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/scope/Scope.scala @@ -1,5 +1,6 @@ package io.joern.javasrc2cpg.scope +import com.github.javaparser.ast.body.Parameter import com.github.javaparser.ast.expr.TypePatternExpr import io.joern.javasrc2cpg.astcreation.ExpectedType import io.joern.javasrc2cpg.scope.Scope.* @@ -40,7 +41,12 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa scopeStack = new FieldDeclScope(isStatic, name) :: scopeStack } - def pushTypeDeclScope(typeDecl: NewTypeDecl, isStatic: Boolean, methodNames: Set[String] = Set.empty): Unit = { + def pushTypeDeclScope( + typeDecl: NewTypeDecl, + isStatic: Boolean, + methodNames: Set[String] = Set.empty, + recordParameters: List[Parameter] = Nil + ): Unit = { val captures = getCapturesForNewScope(isStatic) val outerClassType = scopeStack.takeUntil(_.isInstanceOf[TypeDeclScope]) match { case Nil => None @@ -58,7 +64,8 @@ class Scope(implicit val withSchemaValidation: ValidationMode, val disableTypeFa } .flatten } - scopeStack = new TypeDeclScope(typeDecl, isStatic, captures, outerClassType, methodNames) :: scopeStack + scopeStack = + new TypeDeclScope(typeDecl, isStatic, captures, outerClassType, methodNames, recordParameters) :: scopeStack } def pushNamespaceScope(namespace: NewNamespaceBlock): Unit = { diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MethodParameterTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MethodParameterTests.scala index 5462b381f3b9..f0ce9a6bd5d4 100644 --- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MethodParameterTests.scala +++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MethodParameterTests.scala @@ -19,7 +19,7 @@ class MethodParameterTests2 extends JavaSrcCode2CpgFixture { param.order shouldBe 0 param.index shouldBe 0 param.lineNumber shouldBe Some(3) - param.columnNumber shouldBe None + param.columnNumber shouldBe Some(3) param.typeFullName shouldBe "Foo" param.evaluationStrategy shouldBe EvaluationStrategies.BY_SHARING } diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/RecordTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/RecordTests.scala new file mode 100644 index 000000000000..d2d28406f927 --- /dev/null +++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/RecordTests.scala @@ -0,0 +1,605 @@ +package io.joern.javasrc2cpg.querying + +import io.joern.javasrc2cpg.testfixtures.JavaSrcCode2CpgFixture +import io.shiftleft.codepropertygraph.generated.{ModifierTypes, Operators} +import io.shiftleft.codepropertygraph.generated.nodes.{Call, FieldIdentifier, Identifier, Literal, Method, Return} +import io.shiftleft.semanticcpg.language.* + +class RecordTests extends JavaSrcCode2CpgFixture { + + "a record with a compact constructor" should { + val cpg = code(""" + |package foo; + | + |record Foo(String value) { + | public Foo { + | System.out.println(value); + | } + |} + |""".stripMargin) + + "have the correct representation for the compact constructor" in { + inside(cpg.method.nameExact("").l) { case List(constructor) => + constructor.fullName shouldBe "foo.Foo.:void(java.lang.String)" + + inside(constructor.parameter.l) { case List(thisParam, valueParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + valueParam.name shouldBe "value" + valueParam.typeFullName shouldBe "java.lang.String" + + inside(constructor.body.astChildren.l) { case List(valueAssign: Call, printlnCall: Call) => + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.String" + valueAssign.code shouldBe "this.value = value" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueIdentifier: Identifier) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueIdentifier.name shouldBe "value" + valueIdentifier.typeFullName shouldBe "java.lang.String" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + + printlnCall.name shouldBe "println" + printlnCall.code shouldBe "System.out.println(value)" + inside(printlnCall.argument.l) { case List(_, valueIdentifier: Identifier) => + valueIdentifier.name shouldBe "value" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + } + } + } + } + + "have a private field for the parameter" in { + inside(cpg.member.l) { case List(valueMember) => + valueMember.name shouldBe "value" + valueMember.code shouldBe "String value" + valueMember.typeFullName shouldBe "java.lang.String" + valueMember.modifier.modifierType.l shouldBe List(ModifierTypes.PRIVATE) + } + } + + "have a public accessor method for the parameter" in { + inside(cpg.method.name("value").l) { case List(valueMethod: Method) => + valueMethod.name shouldBe "value" + valueMethod.fullName shouldBe "foo.Foo.value:java.lang.String()" + valueMethod.code shouldBe "public String value()" + valueMethod.lineNumber shouldBe Some(4) + valueMethod.columnNumber shouldBe Some(12) + + val methodReturn = valueMethod.methodReturn + methodReturn.typeFullName shouldBe "java.lang.String" + methodReturn.lineNumber shouldBe Some(4) + methodReturn.columnNumber shouldBe Some(12) + + inside(valueMethod.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + thisParam.lineNumber shouldBe Some(4) + thisParam.columnNumber shouldBe Some(12) + } + + inside(valueMethod.body.astChildren.l) { case List(returnStmt: Return) => + returnStmt.code shouldBe "return this.value" + returnStmt.lineNumber shouldBe Some(4) + returnStmt.columnNumber shouldBe Some(12) + + inside(returnStmt.astChildren.l) { case List(fieldAccess: Call) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.methodFullName shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + fieldAccess.lineNumber shouldBe Some(4) + fieldAccess.columnNumber shouldBe Some(12) + + inside(fieldAccess.argument.l) { case List(thisIdentifier: Identifier, fieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.code shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe cpg.method.name("value").parameter.l + thisIdentifier.lineNumber shouldBe Some(4) + thisIdentifier.columnNumber shouldBe Some(12) + + fieldIdentifier.canonicalName shouldBe "value" + fieldIdentifier.code shouldBe "value" + fieldIdentifier.lineNumber shouldBe Some(4) + fieldIdentifier.columnNumber shouldBe Some(12) + } + } + } + } + } + } + + "a record with an explicit non-canonical constructor" should { + val cpg = code(""" + |package foo; + | + |record Foo(String value) { + | public Foo() { + | this.value = "value"; + | } + |} + |""".stripMargin) + + "have the correct constructors" in { + inside(cpg.method.nameExact("").sortBy(_.parameter.size).l) { + case List(explicitConstructor, canonicalConstructor) => + explicitConstructor.fullName shouldBe "foo.Foo.:void()" + + inside(explicitConstructor.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + inside(explicitConstructor.body.astChildren.l) { case List(valueAssign: Call) => + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.String" + valueAssign.code shouldBe "this.value = \"value\"" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueLiteral: Literal) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueLiteral.typeFullName shouldBe "java.lang.String" + valueLiteral.code shouldBe "\"value\"" + } + } + } + + canonicalConstructor.fullName shouldBe "foo.Foo.:void(java.lang.String)" + + inside(canonicalConstructor.parameter.l) { case List(thisParam, valueParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + valueParam.name shouldBe "value" + valueParam.typeFullName shouldBe "java.lang.String" + + inside(canonicalConstructor.body.astChildren.l) { case List(valueAssign: Call) => + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.String" + valueAssign.code shouldBe "this.value = value" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueIdentifier: Identifier) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueIdentifier.name shouldBe "value" + valueIdentifier.typeFullName shouldBe "java.lang.String" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + } + } + } + } + + "have a private field for the parameter" in { + inside(cpg.member.l) { case List(valueMember) => + valueMember.name shouldBe "value" + valueMember.code shouldBe "String value" + valueMember.typeFullName shouldBe "java.lang.String" + valueMember.modifier.modifierType.l shouldBe List(ModifierTypes.PRIVATE) + } + } + + "have a public accessor method for the parameter" in { + inside(cpg.method.name("value").l) { case List(valueMethod: Method) => + valueMethod.name shouldBe "value" + valueMethod.fullName shouldBe "foo.Foo.value:java.lang.String()" + valueMethod.code shouldBe "public String value()" + valueMethod.lineNumber shouldBe Some(4) + valueMethod.columnNumber shouldBe Some(12) + + val methodReturn = valueMethod.methodReturn + methodReturn.typeFullName shouldBe "java.lang.String" + methodReturn.lineNumber shouldBe Some(4) + methodReturn.columnNumber shouldBe Some(12) + + inside(valueMethod.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + thisParam.lineNumber shouldBe Some(4) + thisParam.columnNumber shouldBe Some(12) + } + + inside(valueMethod.body.astChildren.l) { case List(returnStmt: Return) => + returnStmt.code shouldBe "return this.value" + returnStmt.lineNumber shouldBe Some(4) + returnStmt.columnNumber shouldBe Some(12) + + inside(returnStmt.astChildren.l) { case List(fieldAccess: Call) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.methodFullName shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + fieldAccess.lineNumber shouldBe Some(4) + fieldAccess.columnNumber shouldBe Some(12) + + inside(fieldAccess.argument.l) { case List(thisIdentifier: Identifier, fieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.code shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe cpg.method.name("value").parameter.l + thisIdentifier.lineNumber shouldBe Some(4) + thisIdentifier.columnNumber shouldBe Some(12) + + fieldIdentifier.canonicalName shouldBe "value" + fieldIdentifier.code shouldBe "value" + fieldIdentifier.lineNumber shouldBe Some(4) + fieldIdentifier.columnNumber shouldBe Some(12) + } + } + } + } + } + } + + "a record with an explicit canonical constructor" should { + val cpg = code(""" + |package foo; + | + |record Foo(String value) { + | public Foo(String value) { + | System.out.println(value); + | this.value = value; + | } + |} + |""".stripMargin) + + "have the correct constructor" in { + inside(cpg.method.nameExact("").l) { case List(constructor) => + constructor.fullName shouldBe "foo.Foo.:void(java.lang.String)" + + inside(constructor.parameter.l) { case List(thisParam, valueParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + valueParam.name shouldBe "value" + valueParam.typeFullName shouldBe "java.lang.String" + + inside(constructor.body.astChildren.l) { case List(printlnCall: Call, valueAssign: Call) => + printlnCall.name shouldBe "println" + printlnCall.code shouldBe "System.out.println(value)" + + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.String" + valueAssign.code shouldBe "this.value = value" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueIdentifier: Identifier) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueIdentifier.name shouldBe "value" + valueIdentifier.typeFullName shouldBe "java.lang.String" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + } + } + } + } + + "have a private field for the parameter" in { + inside(cpg.member.l) { case List(valueMember) => + valueMember.name shouldBe "value" + valueMember.code shouldBe "String value" + valueMember.typeFullName shouldBe "java.lang.String" + valueMember.modifier.modifierType.l shouldBe List(ModifierTypes.PRIVATE) + } + } + + "have a public accessor method for the parameter" in { + inside(cpg.method.name("value").l) { case List(valueMethod: Method) => + valueMethod.name shouldBe "value" + valueMethod.fullName shouldBe "foo.Foo.value:java.lang.String()" + valueMethod.code shouldBe "public String value()" + valueMethod.lineNumber shouldBe Some(4) + valueMethod.columnNumber shouldBe Some(12) + + val methodReturn = valueMethod.methodReturn + methodReturn.typeFullName shouldBe "java.lang.String" + methodReturn.lineNumber shouldBe Some(4) + methodReturn.columnNumber shouldBe Some(12) + + inside(valueMethod.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + thisParam.lineNumber shouldBe Some(4) + thisParam.columnNumber shouldBe Some(12) + } + + inside(valueMethod.body.astChildren.l) { case List(returnStmt: Return) => + returnStmt.code shouldBe "return this.value" + returnStmt.lineNumber shouldBe Some(4) + returnStmt.columnNumber shouldBe Some(12) + + inside(returnStmt.astChildren.l) { case List(fieldAccess: Call) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.methodFullName shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + fieldAccess.lineNumber shouldBe Some(4) + fieldAccess.columnNumber shouldBe Some(12) + + inside(fieldAccess.argument.l) { case List(thisIdentifier: Identifier, fieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.code shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe cpg.method.name("value").parameter.l + thisIdentifier.lineNumber shouldBe Some(4) + thisIdentifier.columnNumber shouldBe Some(12) + + fieldIdentifier.canonicalName shouldBe "value" + fieldIdentifier.code shouldBe "value" + fieldIdentifier.lineNumber shouldBe Some(4) + fieldIdentifier.columnNumber shouldBe Some(12) + } + } + } + } + } + } + + "a record with a generic parameter" should { + val cpg = code(""" + |package foo; + | + |record Foo(T value) {} + |""".stripMargin) + + "have the correct default canonical constructor" in { + inside(cpg.method.nameExact("").l) { case List(constructor) => + constructor.fullName shouldBe "foo.Foo.:void(java.lang.Object)" + + inside(constructor.parameter.l) { case List(thisParam, valueParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + valueParam.name shouldBe "value" + valueParam.typeFullName shouldBe "java.lang.Object" + + inside(constructor.body.astChildren.l) { case List(valueAssign: Call) => + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.Object" + valueAssign.code shouldBe "this.value = value" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueIdentifier: Identifier) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.Object" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueIdentifier.name shouldBe "value" + valueIdentifier.typeFullName shouldBe "java.lang.Object" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + } + } + } + } + + "have a private field for the parameter" in { + inside(cpg.member.l) { case List(valueMember) => + valueMember.name shouldBe "value" + valueMember.code shouldBe "T value" + valueMember.typeFullName shouldBe "java.lang.Object" + valueMember.modifier.modifierType.l shouldBe List(ModifierTypes.PRIVATE) + } + } + + "have a public accessor method for the parameter" in { + inside(cpg.method.name("value").l) { case List(valueMethod: Method) => + valueMethod.name shouldBe "value" + valueMethod.fullName shouldBe "foo.Foo.value:java.lang.Object()" + valueMethod.code shouldBe "public T value()" + valueMethod.lineNumber shouldBe Some(4) + valueMethod.columnNumber shouldBe Some(15) + + val methodReturn = valueMethod.methodReturn + methodReturn.typeFullName shouldBe "java.lang.Object" + methodReturn.lineNumber shouldBe Some(4) + methodReturn.columnNumber shouldBe Some(15) + + inside(valueMethod.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + thisParam.lineNumber shouldBe Some(4) + thisParam.columnNumber shouldBe Some(15) + } + + inside(valueMethod.body.astChildren.l) { case List(returnStmt: Return) => + returnStmt.code shouldBe "return this.value" + returnStmt.lineNumber shouldBe Some(4) + returnStmt.columnNumber shouldBe Some(15) + + inside(returnStmt.astChildren.l) { case List(fieldAccess: Call) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.methodFullName shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.Object" + fieldAccess.lineNumber shouldBe Some(4) + fieldAccess.columnNumber shouldBe Some(15) + + inside(fieldAccess.argument.l) { case List(thisIdentifier: Identifier, fieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.code shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe cpg.method.name("value").parameter.l + thisIdentifier.lineNumber shouldBe Some(4) + thisIdentifier.columnNumber shouldBe Some(15) + + fieldIdentifier.canonicalName shouldBe "value" + fieldIdentifier.code shouldBe "value" + fieldIdentifier.lineNumber shouldBe Some(4) + fieldIdentifier.columnNumber shouldBe Some(15) + } + } + } + } + } + } + + "a simple record with no explicit body" should { + val cpg = code(""" + |package foo; + | + |record Foo(String value) {} + |""".stripMargin) + + "have the correct default canonical constructor" in { + inside(cpg.method.nameExact("").l) { case List(constructor) => + constructor.fullName shouldBe "foo.Foo.:void(java.lang.String)" + + inside(constructor.parameter.l) { case List(thisParam, valueParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + + valueParam.name shouldBe "value" + valueParam.typeFullName shouldBe "java.lang.String" + + inside(constructor.body.astChildren.l) { case List(valueAssign: Call) => + valueAssign.name shouldBe Operators.assignment + valueAssign.methodFullName shouldBe Operators.assignment + valueAssign.typeFullName shouldBe "java.lang.String" + valueAssign.code shouldBe "this.value = value" + + inside(valueAssign.argument.l) { case List(fieldAccess: Call, valueIdentifier: Identifier) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + + inside(fieldAccess.argument.l) { + case List(thisIdentifier: Identifier, valueFieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe List(thisParam) + + valueFieldIdentifier.canonicalName shouldBe "value" + } + + valueIdentifier.name shouldBe "value" + valueIdentifier.typeFullName shouldBe "java.lang.String" + valueIdentifier.refsTo.l shouldBe List(valueParam) + } + } + } + } + } + + "have a private field for the parameter" in { + inside(cpg.member.l) { case List(valueMember) => + valueMember.name shouldBe "value" + valueMember.code shouldBe "String value" + valueMember.typeFullName shouldBe "java.lang.String" + valueMember.modifier.modifierType.l shouldBe List(ModifierTypes.PRIVATE) + } + } + + "have a public accessor method for the parameter" in { + inside(cpg.method.name("value").l) { case List(valueMethod: Method) => + valueMethod.name shouldBe "value" + valueMethod.fullName shouldBe "foo.Foo.value:java.lang.String()" + valueMethod.code shouldBe "public String value()" + valueMethod.lineNumber shouldBe Some(4) + valueMethod.columnNumber shouldBe Some(12) + + val methodReturn = valueMethod.methodReturn + methodReturn.typeFullName shouldBe "java.lang.String" + methodReturn.lineNumber shouldBe Some(4) + methodReturn.columnNumber shouldBe Some(12) + + inside(valueMethod.parameter.l) { case List(thisParam) => + thisParam.name shouldBe "this" + thisParam.typeFullName shouldBe "foo.Foo" + thisParam.lineNumber shouldBe Some(4) + thisParam.columnNumber shouldBe Some(12) + } + + inside(valueMethod.body.astChildren.l) { case List(returnStmt: Return) => + returnStmt.code shouldBe "return this.value" + returnStmt.lineNumber shouldBe Some(4) + returnStmt.columnNumber shouldBe Some(12) + + inside(returnStmt.astChildren.l) { case List(fieldAccess: Call) => + fieldAccess.name shouldBe Operators.fieldAccess + fieldAccess.methodFullName shouldBe Operators.fieldAccess + fieldAccess.code shouldBe "this.value" + fieldAccess.typeFullName shouldBe "java.lang.String" + fieldAccess.lineNumber shouldBe Some(4) + fieldAccess.columnNumber shouldBe Some(12) + + inside(fieldAccess.argument.l) { case List(thisIdentifier: Identifier, fieldIdentifier: FieldIdentifier) => + thisIdentifier.name shouldBe "this" + thisIdentifier.code shouldBe "this" + thisIdentifier.typeFullName shouldBe "foo.Foo" + thisIdentifier.refsTo.l shouldBe cpg.method.name("value").parameter.l + thisIdentifier.lineNumber shouldBe Some(4) + thisIdentifier.columnNumber shouldBe Some(12) + + fieldIdentifier.canonicalName shouldBe "value" + fieldIdentifier.code shouldBe "value" + fieldIdentifier.lineNumber shouldBe Some(4) + fieldIdentifier.columnNumber shouldBe Some(12) + } + } + } + } + } + } +}