Skip to content

Commit

Permalink
feat(bulk-model-sync): workaround removed and outdated resolveInfo af…
Browse files Browse the repository at this point in the history
…ter import into MPS

In projects, loading all libraries and plugins can significant time.
This workaround enables to sync projects better that do not want to load all libraries and plugins.
With this workaround, the `name` or `resolveInfo` property of a target is used as the `resoleInfo` property on a reference even if the concept of the target could not be fully loaded.
  • Loading branch information
odzhychko committed Jun 19, 2024
1 parent 5e8a62b commit 4760368
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 29 deletions.
2 changes: 2 additions & 0 deletions bulk-model-sync-lib/mps-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ plugins {

dependencies {
testImplementation(project(":bulk-model-sync-lib"))
testImplementation(project(":bulk-model-sync-mps"))
testImplementation(project(":mps-model-adapters"))
testImplementation(libs.kotlin.serialization.json)
testImplementation(libs.assertj)
}

intellij {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2024.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.model.sync.bulk.lib.test

import kotlinx.serialization.json.Json
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.linesOf
import org.modelix.model.api.BuiltinLanguages
import org.modelix.model.api.INode
import org.modelix.model.data.ModelData
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
import org.modelix.model.sync.bulk.ExistingAndExpectedNode
import org.modelix.model.sync.bulk.asExported
import org.modelix.mps.model.sync.bulk.MPSBulkSynchronizer
import java.nio.file.Path

class ResolveInfoUpdateTest : MPSTestBase() {

fun `test resolve info is updated with name from INamedConcept (testdata ResolveInfoUpdateTest)`() {
val exportedModuleJson = exportModuleJson()
val modifiedModuleJson = exportedModuleJson.replace("referencedNodeA", "referencedNodeANewName")
val modifiedModule: ModelData = Json.decodeFromString(modifiedModuleJson)

val getModulesToImport = { sequenceOf(ExistingAndExpectedNode(getTestModule(), modifiedModule)) }
MPSBulkSynchronizer.importModelsIntoRepository(mpsProject.repository, getTestModule(), false, getModulesToImport)

assertThat(linesOf(getTestModuleXml()))
.contains(""" <ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeANewName" />""")
}

fun `test resolve info is updated with resolveInfo from IResolveInfo (testdata ResolveInfoUpdateTest)`() {
val exportedModuleJson = exportModuleJson()
val modifiedModuleJson = exportedModuleJson.replace("referencedNodeC", "referencedNodeCNewName")
val modifiedModule: ModelData = Json.decodeFromString(modifiedModuleJson)

val getModulesToImport = { sequenceOf(ExistingAndExpectedNode(getTestModule(), modifiedModule)) }
MPSBulkSynchronizer.importModelsIntoRepository(mpsProject.repository, getTestModule(), false, getModulesToImport)

assertThat(linesOf(getTestModuleXml()))
.contains(""" <ref role="3SLt5I" node="3vHUMVfa0RZ" resolve="referencedNodeCNewName" />""")
}

private fun getTestModule(): INode {
var result: INode? = null
mpsProject.repository.modelAccess.runReadAction {
val repository = mpsProject.repository
val repositoryNode = MPSRepositoryAsNode(repository)
result = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules)
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "NewSolution" }
}
return checkNotNull(result)
}

private fun getTestModuleXml(): Path {
return projectDir.resolve("solutions/NewSolution/models/NewSolution.a_model.mps")
}

private fun exportModuleJson(): String {
var result: String? = null
mpsProject.repository.modelAccess.runReadAction {
val module = getTestModule()
val modelData = ModelData(root = module.asExported())
result = modelData.toJson()
}
return checkNotNull(result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MigrationProperties">
<entry key="project.baseline.version" value="211" />
</component>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MPSProject">
<projectModules>
<modulePath path="$PROJECT_DIR$/solutions/NewSolution/NewSolution.msd" folder="" />
</projectModules>
</component>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<solution name="NewSolution" uuid="471b29cb-3253-460b-9743-1e1443884a6b" moduleVersion="0" compileInMPS="true">
<models>
<modelRoot contentPath="${module}" type="default">
<sourceRoot location="models" />
</modelRoot>
</models>
<facets>
<facet type="java">
<classes generated="true" path="${module}/classes_gen" />
</facet>
</facets>
<sourcePath />
<languageVersions />
<dependencyVersions>
<module reference="471b29cb-3253-460b-9743-1e1443884a6b(NewSolution)" version="0" />
</dependencyVersions>
</solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<model ref="r:cd78e6ac-0e34-490a-9b49-e5643f948d6d(NewSolution.a_model)">
<persistence version="9" />
<languages>
<use id="e2840528-cf1a-4707-9968-32c55e0e5b6c" name="NewLanguage" version="0" />
</languages>
<imports />
<registry>
<language id="ceab5195-25ea-4f22-9b92-103b95ca8c0c" name="jetbrains.mps.lang.core">
<concept id="1196978630214" name="jetbrains.mps.lang.core.structure.IResolveInfo" flags="ng" index="2Lv6Xg">
<property id="1196978656277" name="resolveInfo" index="2Lvdk3" />
</concept>
<concept id="1169194658468" name="jetbrains.mps.lang.core.structure.INamedConcept" flags="ng" index="TrEIO">
<property id="1169194664001" name="name" index="TrG5h" />
</concept>
</language>
<language id="e2840528-cf1a-4707-9968-32c55e0e5b6c" name="NewLanguage">
<concept id="4030135827843012252" name="NewLanguage.structure.RootNode" flags="ng" index="3SLrQM">
<child id="4030135827843012255" name="referencedNode" index="3SLrQL" />
<child id="4030135827843012253" name="referencingNodes" index="3SLrQN" />
</concept>
<concept id="4030135827842946229" name="NewLanguage.structure.ReferencingNode" flags="ng" index="3SMFYr">
<reference id="4030135827843004992" name="aReference" index="3SLt5I" />
</concept>
<concept id="4030135827842946260" name="NewLanguage.structure.ReferencedNodeWithResolveInfo" flags="ng" index="3SMFZU" />
<concept id="4030135827842946256" name="NewLanguage.structure.ReferencedNodeWithName" flags="ng" index="3SMFZY" />
</language>
</registry>
<node concept="3SLrQM" id="3vHUMVfa5C_">
<node concept="3SMFYr" id="3vHUMVfa0RX" role="3SLrQN" >
<ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeA" />
</node>
<node concept="3SMFZY" id="3vHUMVfa0RY" role="3SLrQL">
<property role="TrG5h" value="referencedNodeA" />
</node>
<node concept="3SMFZU" id="3vHUMVfa0RZ" role="3SLrQL">
<property role="2Lvdk3" value="referencedNodeC" />
</node>
<node concept="3SMFYr" id="3vHUMVfa4pM" role="3SLrQN">
<ref role="3SLt5I" node="3vHUMVfa0RZ" resolve="referencedNodeC" />
</node>
</node>
</model>
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package org.modelix.mps.model.sync.bulk

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.ProjectManager
import jetbrains.mps.ide.ThreadUtils
import jetbrains.mps.ide.project.ProjectHelper
import jetbrains.mps.smodel.SNodeUtil
import jetbrains.mps.smodel.StaticReference
import jetbrains.mps.smodel.adapter.ids.MetaIdHelper
import jetbrains.mps.smodel.adapter.ids.SConceptId
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory
import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById
import jetbrains.mps.smodel.language.ConceptRegistry
import jetbrains.mps.smodel.language.StructureRegistry
Expand All @@ -33,6 +35,7 @@ import kotlinx.serialization.json.decodeFromStream
import org.jetbrains.mps.openapi.model.EditableSModel
import org.jetbrains.mps.openapi.module.SModule
import org.jetbrains.mps.openapi.module.SRepository
import org.modelix.model.api.INode
import org.modelix.model.data.ModelData
import org.modelix.model.mpsadapters.MPSModuleAsNode
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
Expand All @@ -43,6 +46,32 @@ import org.modelix.model.sync.bulk.isModuleIncluded
import java.io.File
import java.util.concurrent.atomic.AtomicInteger

/**
* Identifier of the `name` property in the `INamedConcept` concept.
* See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
*/
@Suppress("MagicNumber")
private val namePropertyOfINamedConceptConcept = MetaAdapterFactory.getProperty(
-0x3154ae6ada15b0deL,
-0x646defc46a3573f4L,
0x110396eaaa4L,
0x110396ec041L,
"name",
)

/**
* Identifier of the `resolveInfo` property in the `IResolveInfoConcept` concept.
* See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
*/
@Suppress("MagicNumber")
private val resolveInfoPropertyOfIResolveInfoConcept = MetaAdapterFactory.getProperty(
-0x3154ae6ada15b0deL,
-0x646defc46a3573f4L,
0x116b17c6e46L,
0x116b17cd415L,
"resolveInfo",
)

object MPSBulkSynchronizer {

@JvmStatic
Expand Down Expand Up @@ -94,56 +123,114 @@ object MPSBulkSynchronizer {
if (jsonFiles.isNullOrEmpty()) error("no json files found for included modules")

println("Found ${jsonFiles.size} modules to be imported")
val access = repository.modelAccess
access.executeCommandInEDT {
val getModulesToImport = {
val allModules = repository.modules
val includedModules: Iterable<SModule> = allModules.filter {
isModuleIncluded(it.moduleName!!, includedModuleNames, includedModulePrefixes)
}
val numIncludedModules = includedModules.count()
val repoAsNode = MPSRepositoryAsNode(repository)
println("Importing modules...")
try {
val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
println("Importing module ${index + 1} of $numIncludedModules: '${module.moduleName}'")
val fileName = inputPath + File.separator + module.moduleName + ".json"
val moduleFile = File(fileName)
if (moduleFile.exists()) {
val expectedData: ModelData = moduleFile.inputStream().use(Json::decodeFromStream)
sequenceOf(ExistingAndExpectedNode(MPSModuleAsNode(module), expectedData))
} else {
println("Skip importing ${module.moduleName}} because $fileName does not exist.")
sequenceOf()
}
}
modulesToImport
}
importModelsIntoRepository(repository, MPSRepositoryAsNode(repository), continueOnError, getModulesToImport)
}

/**
* Import specified models into the repository.
* [getModulesToImport] is a lambda to be executed with read access in MPS.
*/
@JvmStatic
fun importModelsIntoRepository(
repository: SRepository,
rootOfImport: INode,
continueOnError: Boolean,
getModulesToImport: () -> Sequence<ExistingAndExpectedNode>,
) {
val access = repository.modelAccess
ThreadUtils.runInUIThreadAndWait {
access.executeCommand {
println("Importing modules...")
// `modulesToImport` lazily produces modules to import
// so that loaded model data can be garbage collected.
val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
println("Importing module ${index + 1} of $numIncludedModules: '${module.moduleName}'")
val fileName = inputPath + File.separator + module.moduleName + ".json"
val moduleFile = File(fileName)
if (moduleFile.exists()) {
val expectedData: ModelData = moduleFile.inputStream().use(Json::decodeFromStream)
sequenceOf(ExistingAndExpectedNode(MPSModuleAsNode(module), expectedData))
} else {
println("Skip importing ${module.moduleName}} because $fileName does not exist.")
sequenceOf()
}
try {
println("Importing modules...")
// `modulesToImport` lazily produces modules to import
// so that loaded model data can be garbage collected.
val modulesToImport = getModulesToImport()
ModelImporter(rootOfImport, continueOnError).importIntoNodes(modulesToImport)
println("Import finished.")
} catch (ex: Exception) {

Check warning

Code scanning / detekt

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled. Warning

The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.
// Exceptions are only visible in the MPS log file by default
ex.printStackTrace()
}
ModelImporter(repoAsNode, continueOnError).importIntoNodes(modulesToImport)
println("Import finished.")
} catch (ex: Exception) {
// Exceptions are only visible in the MPS log file by default
ex.printStackTrace()
}
println("Import finished.")
}

ApplicationManager.getApplication().invokeAndWait {
ThreadUtils.runInUIThreadAndWait {
println("Persisting changes...")
access.executeCommandInEDT {
access.executeCommand {
enableWorkaroundForFilePerRootPersistence(repository)
updateUnsetResolveInfo(repository)
repository.saveAll()
}
println("Changes persisted.")
}
}

/**
* Workaround for MPS not being able to set the `resolveInfo` property on a reference.
* This is the case when the concept of the target node cannot be loaded/is not valid.
* Without this workaround, the `resolve` attribute in serialized references
* (e.g. <ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeA" />)
* will not be set or updated.
*
* The workaround follows the logic of MPS but without relying on the concept being loaded/valid.
* Without this workaround a bulk sync can remove the `resolve` info unintentionally
* and produce unwanted file changes.
*/
private fun updateUnsetResolveInfo(repository: SRepository) {
val changedModels = repository.modules.asSequence()
.flatMap { it.models }
.mapNotNull { it as? EditableSModel }
.filter { it.isChanged }
val references = changedModels
.flatMap { org.jetbrains.mps.openapi.model.SNodeUtil.getDescendants(it) }
.flatMap { it.references }
.mapNotNull { it as? StaticReference }

references.forEach { reference ->
val target = reference.targetNode ?: return@forEach
// A concept is not valid, for example, when the language could not be loaded.
if (target.concept.isValid) {
return@forEach
}
// Try guessing the resolve info following the logic of MPS.
// Use the logic like in MPS but without relying on the concept being loaded.
// https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L230
val newResolveInfo = target.getProperty(resolveInfoPropertyOfIResolveInfoConcept)
?: target.getProperty(namePropertyOfINamedConceptConcept)
if (newResolveInfo != reference.resolveInfo) {
reference.resolveInfo = newResolveInfo
}
}
}

/**
* Workaround for MPS not being able to read the name property of the node during the save process
* in case FilePerRootPersistence is used.
* This is because the concept is not properly loaded and in the MPS code it checks if the concept is a subconcept
* of INamedConcept.
* Without this workaround the id of the root node will be used instead of the name, resulting in renamed files.
* This is because the concept is not properly loaded,
* and in the MPS code it checks if the concept is a subconcept of INamedConcept.
* Without this workaround, the id of the root node will be used instead of the name, resulting in renamed files.
*/
@JvmStatic
private fun enableWorkaroundForFilePerRootPersistence(repository: SRepository) {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,5 @@ micrometer-registry-prometheus = { group = "io.micrometer", name = "micrometer-r

detekt-api = { group = "io.gitlab.arturbosch.detekt", name= "detekt-api", version.ref = "detekt" }
detekt-test = { group = "io.gitlab.arturbosch.detekt", name= "detekt-test", version.ref = "detekt" }

assertj = { group = "org.assertj", name = "assertj-core", version = "3.25.1"}

0 comments on commit 4760368

Please sign in to comment.