Skip to content

Commit

Permalink
Merge pull request #22 from jetbrains-academy/intellij-java-module
Browse files Browse the repository at this point in the history
Splitting intellij module into 3 modules: core, java-psi, kotlin-psi
  • Loading branch information
nbirillo authored Dec 15, 2023
2 parents f852ad1 + 94dce6d commit 282faa4
Show file tree
Hide file tree
Showing 27 changed files with 917 additions and 262 deletions.
15 changes: 13 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import java.util.Properties

plugins {
kotlin("jvm") version "1.8.20"
java
id("io.gitlab.arturbosch.detekt") version "1.21.0"
`maven-publish`
}
Expand All @@ -12,6 +13,7 @@ version = "2.1.0"
allprojects {
apply {
plugin("kotlin")
plugin("java")
plugin("io.gitlab.arturbosch.detekt")
}

Expand All @@ -23,6 +25,11 @@ allprojects {
testRuntimeOnly("org.junit.platform:junit-platform-console:1.9.2")
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

repositories {
mavenCentral()
}
Expand Down Expand Up @@ -65,10 +72,14 @@ val spaceUsername = getLocalProperty("spaceUsername")
val spacePassword = getLocalProperty("spacePassword")

configure(subprojects) {
apply(plugin = "maven-publish")

val subprojectName = this.name

// We don't need to publish the root ij project
if (subprojectName == "ij") {
return@configure
}

apply(plugin = "maven-publish")
publishing {
publications {
register<MavenPublication>("maven") {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ kotlin.code.style=official
pluginName = Kotlin Test Framework With IJ
platformType = IC
platformVersion = 2022.2.5
platformPlugins = Kotlin
platformPlugins = Kotlin, java

org.gradle.jvmargs = -XX:MaxMetaspaceSize=1g
18 changes: 11 additions & 7 deletions ij/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ group = rootProject.group
version = rootProject.version

plugins {
id("org.jetbrains.intellij") version "1.14.1"
id("org.jetbrains.intellij") version "1.15.0"
}

fun properties(key: String) = providers.gradleProperty(key)

intellij {
pluginName = properties("pluginName")
version = properties("platformVersion")
type = properties("platformType")
allprojects {
apply(plugin = "org.jetbrains.intellij")

// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) }
intellij {
pluginName = properties("pluginName")
version = properties("platformVersion")
type = properties("platformType")

// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) }
}
}
2 changes: 2 additions & 0 deletions ij/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
group = rootProject.group
version = rootProject.version
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.jetbrains.academy.test.system.ij.analyzer

import com.intellij.psi.PsiElement

/**
* Retrieves the constant value of the PsiElement.
*
* @param constantExpressionClass The classes of the constant expressions to search for.
* @return The constant value as a string, or null if no constant value is found.
* @throws IllegalArgumentException If the PsiElement contains multiple constant values.
*/
fun <T : PsiElement> PsiElement.getConstValue(vararg constantExpressionClass: Class<out T>): String? {
val possibleValue = extractElementsOfTypes(*constantExpressionClass)
if (possibleValue.isEmpty()) {
return null
}
require(possibleValue.size == 1) { "Parser error! A const variable must have only one value" }
return possibleValue.first().text.trimIndent()
}

/**
* Retrieves the body of a block represented by the given code block class.
*
* @param codeBlockClass the class representing the code block
* @return the body of the block as a string, or null if nobody is found
*/
fun <T : PsiElement> PsiElement.getBlockBody(codeBlockClass: Class<T>): String? {
val possibleBody = extractElementsOfTypes(codeBlockClass)
if (possibleBody.isEmpty()) {
return null
}
return possibleBody.first().text.trimBraces().trimIndent()
}

/**
* Trims leading and trailing braces from a string.
*/
private fun String.trimBraces() = dropWhile { it.isWhitespace() }.removePrefix("{")
.dropLastWhile { it.isWhitespace() }.removeSuffix("}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.jetbrains.academy.test.system.ij.analyzer

import com.intellij.openapi.application.ApplicationManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.parentsOfType

/** Extracts elements of given type from related files in project. */
fun <T : PsiElement> PsiElement.extractElementsOfTypes(vararg psiElementClass: Class<out T>): MutableCollection<T> =
psiElementClass.flatMap { PsiTreeUtil.collectElementsOfType(this, it) }.toMutableList()

/**
* Checks if the PsiFile contains an element of the specified type and with the given name.
*
* @param T the type of the PsiNamedElement subclass to check for
* @param psiElementClass the class object representing the type of the element to check for
* @param name the name of the element to search for
* @return true if an element of the specified type and name exists in the PsiFile, false otherwise
*/
fun <T : PsiNamedElement> PsiFile.hasElementOfTypeWithName(psiElementClass: Class<out T>, name: String): Boolean =
ApplicationManager.getApplication().runReadAction<Boolean> {
extractElementsOfTypes(psiElementClass).any { it.name == name }
}

/**
* Retrieves the text of the parent element of the given element.
*
* @param element The element for which to retrieve the parent text.
* @param isParentTypeFunction True if the parent element's type is a function; false otherwise.
* @param parentClass The class of the parent element.
* @return The text of the parent element, or null if the parent does not exist or is not of the specified type.
*/
private fun <T : PsiNamedElement> getParentText(
element: PsiElement,
isParentTypeFunction: Boolean,
parentClass: Class<out T>
): String? {
return if (isParentTypeFunction) {
element.parentsOfType(parentClass).firstOrNull()?.name
} else {
element.parent?.text
}
}

/**
* Checks if the given PsiFile has an expression with the specified parent.
*
* @param expression The text of the expression to search for.
* @param parent The text of the parent to compare with.
* @param isParentTypeFunction Specifies if the parent should be treated as a function or not.
* @param parentClass The class of the parent element.
* @param expressionClass The classes of the expression elements to search for.
* @return true if the PsiFile has an expression with the specified parent, false otherwise.
*/
fun <T : PsiElement, V : PsiNamedElement> PsiFile.hasExpressionWithParent(
expression: String,
parent: String?,
isParentTypeFunction: Boolean,
parentClass: Class<out V>,
vararg expressionClass: Class<out T>
): Boolean =
ApplicationManager.getApplication().runReadAction<Boolean> {
val expressions: MutableCollection<PsiElement> = extractElementsOfTypes(*expressionClass)
expressions.any { it.text == expression && getParentText(it, isParentTypeFunction, parentClass) == parent }
}

/**
* Finds usages of a given method in the PSI file.
*
* @param methodName The name of the method to find usages of.
* @param callExpressionClass The class representing the call expression to search for.
* @param methodClass The class representing the method to search for usages of.
* @return A list of strings representing the names of the methods in which the given method is used.
*/
fun <T : PsiElement, V : PsiNamedElement> PsiFile.findMethodUsages(
methodName: String,
callExpressionClass: Class<T>,
methodClass: Class<V>
): List<String> =
ApplicationManager.getApplication().runReadAction<List<String>> {
val callExpression = extractElementsOfTypes(callExpressionClass)
callExpression.filter { it.text == methodName }.mapNotNull {
it.parentsOfType(methodClass).firstOrNull()?.name
}.toList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleManager
import org.jetbrains.academy.test.system.inspections.applyInspections
import org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection
import org.junit.jupiter.api.Assertions

// TODO: make it possible to check different aspects of formatting
Expand All @@ -32,10 +30,3 @@ fun PsiFile.formatting(): String? {
}
return ApplicationManager.getApplication().runReadAction<String> { text }
}

fun PsiFile.checkIfOptimizeImportsWereApplied() {
Assertions.assertTrue(
applyInspections(listOf(KotlinUnusedImportInspection())).isEmpty(),
"Please, apply \"Optimize import\" option when formatting code."
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.PsiFile

fun PsiFile.applyInspections(inspections: List<LocalInspectionTool>): List<ProblemDescriptor> {
fun PsiFile.applyLocalInspections(inspections: List<LocalInspectionTool>): List<ProblemDescriptor> {
val problems: MutableList<ProblemDescriptor> = mutableListOf()
val inspectionManager = InspectionManager.getInstance(project)
ProgressManager.getInstance().executeProcessUnderProgress(
Expand Down
6 changes: 6 additions & 0 deletions ij/java-psi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
group = rootProject.group
version = rootProject.version

dependencies {
implementation(project(":ij:common"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.jetbrains.academy.test.system.java.ij.analyzer

import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiCodeBlock
import org.jetbrains.academy.test.system.ij.analyzer.extractElementsOfTypes
import org.jetbrains.academy.test.system.ij.analyzer.getBlockBody
import org.jetbrains.academy.test.system.ij.analyzer.getConstValue
import org.jetbrains.academy.test.system.ij.formatting.formatting

/**
* Checks if PsiFile contains a constant property with the given element value.
*
* @param elementValue The value to search for in the constant properties.
* @return true if a constant property with the specified value is found, false otherwise.
*/
fun PsiFile.hasConstantWithGivenValue(elementValue: String): Boolean =
ApplicationManager.getApplication().runReadAction<Boolean> {
val elements = extractElementsOfTypes(PsiField::class.java)
elements.any { it.modifierList?.text?.contains("final") ?: false && it.getConstValue(PsiLiteralExpression::class.java) == elementValue }
}

/**
* Formatting code content.
*
* @param content The source code to wrap and format.
* @param project An instance of the project, used for creating PsiFile.
* @return The formatted code content.
*/
private fun formattingContent(content: String, project: Project): String {
val wrappedCode = "class WrappedClass { void wrappedMethod() { $content } }"
val factory = PsiFileFactory.getInstance(project)
val contentPsiFile = factory.createFileFromText("Content.java", JavaFileType.INSTANCE, wrappedCode)
val formattingContent = contentPsiFile.formatting() ?: ""
return formattingContent.lines().drop(2).dropLast(2).joinToString(System.lineSeparator()).trimIndent()
}

/**
* Finds methods within the given PsiFile that have the specified body content.
*
* @param content The body content to search for in the methods.
* @return A list of method names whose bodies match the provided content.
*/
fun PsiFile.findMethodsWithContent(content: String): List<String> =
ApplicationManager.getApplication().runReadAction<List<String>> {
formatting()
val formattingContent = formattingContent(content, project)

val methods = extractElementsOfTypes(PsiMethod::class.java)
methods.filter { it.getBlockBody(PsiCodeBlock::class.java) == formattingContent }.mapNotNull { it.name }.toList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jetbrains.academy.test.system.java.ij.formatting

import com.intellij.psi.PsiFile
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.psi.codeStyle.JavaCodeStyleManager

fun PsiFile.checkIfOptimizeImportsWereApplied() {
val originalCode = ApplicationManager.getApplication().runReadAction<String> { text }
WriteCommandAction.runWriteCommandAction(project) {
JavaCodeStyleManager.getInstance(project).optimizeImports(this)
}
val optimizedCode = ApplicationManager.getApplication().runReadAction<String> { text }
assert(originalCode.trimIndent() == optimizedCode.trimIndent()) {
"Please, apply \"Optimize import\" option when formatting code."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.jetbrains.academy.test.system.java.test

import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiNewExpression
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiCallExpression
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import org.jetbrains.academy.test.system.ij.analyzer.findMethodUsages
import org.jetbrains.academy.test.system.ij.analyzer.hasElementOfTypeWithName
import org.jetbrains.academy.test.system.ij.analyzer.hasExpressionWithParent
import org.jetbrains.academy.test.system.java.ij.analyzer.findMethodsWithContent
import org.jetbrains.academy.test.system.java.ij.analyzer.hasConstantWithGivenValue

/**
* A Base test class to create tests with PSI without adding intellij dependency into courses directly.
*/
open class BaseIjTestClass : BasePlatformTestCase() {

fun hasConstantWithGivenValue(elementValue: String): Boolean =
myFixture.file.hasConstantWithGivenValue(elementValue)

fun findMethodsWithContent(content: String): List<String> =
myFixture.file.findMethodsWithContent(content)

fun findMethodUsages(content: String): List<String> =
myFixture.file.findMethodUsages(content, PsiCallExpression::class.java, PsiMethod::class.java)

fun hasProperty(propertyName: String): Boolean =
myFixture.file.hasElementOfTypeWithName(PsiField::class.java, propertyName)

fun hasMethod(methodName: String): Boolean =
myFixture.file.hasElementOfTypeWithName(PsiMethod::class.java, methodName)

fun hasClass(className: String): Boolean = myFixture.file.hasElementOfTypeWithName(PsiClass::class.java, className)

fun hasParameter(parameterName: String): Boolean =
myFixture.file.hasElementOfTypeWithName(PsiParameter::class.java, parameterName)

fun hasExpressionWithParent(expression: String, parent: String?, isParentTypeFunction: Boolean = false): Boolean =
myFixture.file.hasExpressionWithParent(
expression, parent, isParentTypeFunction, PsiMethod::class.java,
PsiNewExpression::class.java, PsiReferenceExpression::class.java, PsiMethodCallExpression::class.java
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.jetbrains.academy.test.system.java.ij

import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import org.junit.Ignore

@Ignore
open class BaseFormattingUtilTests : BasePlatformTestCase() {

protected fun testWithError(code: String, action: (PsiFile) -> Unit) {
assertThrows(AssertionError::class.java) { action(myFixture.configureByText("dummy.java", code)) }
}

protected fun testWithoutError(code: String, action: (PsiFile) -> Unit) {
action(myFixture.configureByText("dummy.java", code))
}
}
Loading

0 comments on commit 282faa4

Please sign in to comment.