From 5121d072c954146acd11916459eaf14590658af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 21:55:39 +0100 Subject: [PATCH] Resolve a serialized model reference in a model target by model ID A model target that parses as a model reference with a type designator now resolves by model ID, so a reference with an out-of-date name still finds its model and a reference to a missing model never falls back to a name lookup. Other targets are model names, as before. The navigation resolver used by search scopes shares the same parser. --- .../mops/cli/check/ModelCheckCommand.kt | 2 +- .../mops/daemon/ModelNodeResolver.kt | 17 +++++----- .../mops/daemon/NavigationTargetResolver.kt | 8 +---- .../mops/daemon/modelReferenceSpelling.kt | 18 +++++++++++ .../mops/daemon/CodeModeLookupTest.kt | 2 ++ .../mops/daemon/GetNodeSemanticsTest.kt | 31 ++++++++++++++++++- .../mops/daemon/ModelCheckSemanticsTest.kt | 9 ++++++ docs/reference/check.md | 3 +- 8 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 daemon/src/main/kotlin/com/specificlanguages/mops/daemon/modelReferenceSpelling.kt diff --git a/cli/src/main/kotlin/com/specificlanguages/mops/cli/check/ModelCheckCommand.kt b/cli/src/main/kotlin/com/specificlanguages/mops/cli/check/ModelCheckCommand.kt index 893e52af..8d028d53 100644 --- a/cli/src/main/kotlin/com/specificlanguages/mops/cli/check/ModelCheckCommand.kt +++ b/cli/src/main/kotlin/com/specificlanguages/mops/cli/check/ModelCheckCommand.kt @@ -21,7 +21,7 @@ class ModelCheckCommand(environment: CommandEnvironment) : CheckCommand(environm index = "0", arity = "1", paramLabel = "MODEL_TARGET", - description = ["Model name or serialized model reference."], + description = ["Model name, or serialized model reference (resolved by model ID)."], ) lateinit var target: String diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelNodeResolver.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelNodeResolver.kt index abf4dcec..6aa9f057 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelNodeResolver.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelNodeResolver.kt @@ -59,13 +59,16 @@ class ModelNodeResolver( return model } - private fun matchingModels(project: Project, modelTarget: String): List = - modelCandidates(project) - .filter { model -> - model.name.value == modelTarget || - persistence.asString(model.reference) == modelTarget - } - .toList() + // A target that parses as a serialized model reference is that reference and resolves by model ID alone, so a + // reference whose name part is out of date still finds its model. It never falls back to a name lookup. Any other + // target is a model name. + private fun matchingModels(project: Project, modelTarget: String): List { + val reference = parseModelReferenceOrNull(persistence, modelTarget) + if (reference != null) { + return listOfNotNull(reference.resolve(project.repository)) + } + return modelCandidates(project).filter { it.name.value == modelTarget }.toList() + } private fun logMissingModelTarget(project: Project, modelTarget: String) { val candidates = modelCandidates(project).toList() diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/NavigationTargetResolver.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/NavigationTargetResolver.kt index 80bf095b..92b8e87e 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/NavigationTargetResolver.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/NavigationTargetResolver.kt @@ -82,14 +82,8 @@ internal class NavigationTargetResolver( persistence.createNodeReference(normalizeNodeReferenceSpelling(persistence, target)).resolve(project.repository) }.getOrNull() - // A bare model name parses into a reference whose id is derived from the name. MPS 2024.1 resolves such an id - // inexactly, to any model of that name; later versions resolve it only to a model with that exact id. Requiring the - // ids to match gives the later behavior on every version, so a bare name is never taken as a model reference. fun resolveModelReference(target: String): SModel? = - runCatching { - val reference = persistence.createModelReference(target) - reference.resolve(project.repository)?.takeIf { it.reference.modelId == reference.modelId } - }.getOrNull() + parseModelReferenceOrNull(persistence, target)?.resolve(project.repository) fun resolveModuleReference(target: String): SModule? = runCatching { diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/modelReferenceSpelling.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/modelReferenceSpelling.kt new file mode 100644 index 00000000..66975cc2 --- /dev/null +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/modelReferenceSpelling.kt @@ -0,0 +1,18 @@ +package com.specificlanguages.mops.daemon + +import org.jetbrains.mps.openapi.model.SModelReference +import org.jetbrains.mps.openapi.persistence.PersistenceFacade + +/** + * Parses [text] as a serialized **Model Reference** when its model ID carries a type designator (`r:`, `f:`, `path:`, + * `java:` and so on), optionally after a `moduleId/` prefix and before a `(model name)` part. Returns null for any other + * text, and for text MPS cannot parse. + * + * MPS's `createModelReference` reads text without a type designator as a name-based model ID. mops does not support + * name-based model IDs, so such text never reaches the MPS parser here and callers read it as a model name instead. + */ +internal fun parseModelReferenceOrNull(persistence: PersistenceFacade, text: String): SModelReference? { + val modelId = text.trim().substringBefore('(').substringAfter('/') + if (':' !in modelId) return null + return runCatching { persistence.createModelReference(text) }.getOrNull() +} diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/CodeModeLookupTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/CodeModeLookupTest.kt index 0dbba61f..add168fe 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/CodeModeLookupTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/CodeModeLookupTest.kt @@ -37,6 +37,8 @@ class CodeModeLookupTest { assert mops.lookup.module(module.moduleName).is(module) def model = mops.lookup.requireModel('com.specificlanguages.json.structure') assert mops.lookup.model(model.reference.toString()).is(model) + assert mops.lookup.model('r:fd752404-89d3-4ffe-bc3a-7fb7a27c63b6(com.specificlanguages.json.oldname)').is(model) + assert mops.lookup.model('r:99999999-2222-4333-8444-555555555555(com.specificlanguages.json.structure)') == null def concept = mops.lookup.requireConceptByName('jetbrains.mps.baseLanguage.ClassConcept') assert mops.lookup.conceptByName('jetbrains.mps.baseLanguage.ClassConcept') == concept } diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/GetNodeSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/GetNodeSemanticsTest.kt index 2eac3f16..52b5548e 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/GetNodeSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/GetNodeSemanticsTest.kt @@ -90,6 +90,28 @@ class GetNodeSemanticsTest { assertEquals("JsonFile", propertyValue(node, "name")) } + @Test + fun `resolves a model reference with an out-of-date name by its model ID`() { + val staleReference = "r:fd752404-89d3-4ffe-bc3a-7fb7a27c63b6(com.specificlanguages.json.oldname)" + + val node = getNodeInSharedProject(NodeTarget.InModel(staleReference, JSON_FILE_NODE_ID)) + + assertEquals(STRUCTURE_MODEL_REFERENCE, node.model) + assertEquals("JsonFile", propertyValue(node, "name")) + } + + @Test + fun `fails on a model reference to a missing model without falling back to its name`() { + // The name part names an existing model; only the model ID is unknown. + val missingReference = "r:99999999-2222-4333-8444-555555555555(com.specificlanguages.json.structure)" + + val exception = assertFailsWith { + getNodeInSharedProject(NodeTarget.InModel(missingReference, JSON_FILE_NODE_ID)) + } + + assertEquals(MpsErrorCode.MODEL_NOT_FOUND, exception.code) + } + @Test fun `matches model target by model name value instead of long name`() { val stereotypedReference = "r:11111111-2222-4333-8444-555555555555(com.specificlanguages.json.structure@tests)" @@ -305,7 +327,7 @@ class GetNodeSemanticsTest { model.resolveSibling("duplicate.structure.mps").writeText( model.readText().replace( STRUCTURE_MODEL_REFERENCE, - "r:11111111-2222-4333-8444-555555555555(com.specificlanguages.json.structure)", + DUPLICATE_MODEL_REFERENCE, ), ) }, @@ -318,6 +340,12 @@ class GetNodeSemanticsTest { assertEquals(MpsErrorCode.AMBIGUOUS_TARGET, exception.code) assertContains(exception.message, "ambiguous model target") + + // A model reference names one of the two same-named models by its ID. + val duplicate = mpsAccess.read { + getNode(NodeTarget.InModel(DUPLICATE_MODEL_REFERENCE, JSON_FILE_NODE_ID)) + } + assertEquals(DUPLICATE_MODEL_REFERENCE, duplicate.model) } } @@ -345,5 +373,6 @@ class GetNodeSemanticsTest { const val EDITOR_ROOT_NODE_ID = "1P8oQ4NaXEq" const val EDITOR_COLLECTION_NODE_ID = "1P8oQ4NaXEs" const val STRUCTURE_MODEL_PATH = "languages/com.specificlanguages.json/models/com.specificlanguages.json.structure.mps" + const val DUPLICATE_MODEL_REFERENCE = "r:11111111-2222-4333-8444-555555555555(com.specificlanguages.json.structure)" } } diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCheckSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCheckSemanticsTest.kt index 47287099..73dd5309 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCheckSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCheckSemanticsTest.kt @@ -74,6 +74,15 @@ class ModelCheckSemanticsTest { assertEquals(MpsErrorCode.MODEL_NOT_FOUND, failure.code) } + @Test + fun `a model reference with an out-of-date name checks the model it identifies`() { + val response = SharedMpsEnvironment.withProjectCopy(projectName = SANDBOX) { access, _ -> + access.read { checkModel(MODEL_REFERENCE.replace("(baselanguage.sandbox)", "(baselanguage.oldname)"), limit = 0) } + } + + assertEquals(1, response.summary.rootsChecked) + } + @Test fun `project check checks models in every Project Module`() { val response = SharedMpsEnvironment.withProjectCopy( diff --git a/docs/reference/check.md b/docs/reference/check.md index f49b0c68..83566235 100644 --- a/docs/reference/check.md +++ b/docs/reference/check.md @@ -25,7 +25,8 @@ Run MPS's full model check over one model. Usage: mops check model [-h] [--format=FORMAT] [--limit=N] MODEL_TARGET Run MPS's full model check over one model and report the findings. Parameters: - MODEL_TARGET Model name or serialized model reference. + MODEL_TARGET Model name, or serialized model reference (resolved + by model ID). Options: --format=FORMAT How to print findings: human (a readable list sorted by severity, then # metadata lines;