Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,16 @@ class ModelNodeResolver(
return model
}

private fun matchingModels(project: Project, modelTarget: String): List<SModel> =
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<SModel> {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MpsRequestException> {
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)"
Expand Down Expand Up @@ -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,
),
)
},
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/check.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading