Skip to content

Repository files navigation

mps-pre-commit-hooks

Reusable pre-commit / prek hooks for JetBrains MPS projects.

MPS keeps a lot of its project structure in files that are easy to leave in an inconsistent state by hand or by a bad merge: a module added on disk but never registered in .mps/modules.xml, a model dropped outside every source root, a descriptor renamed but not its folder. MPS silently ignores most of these, so the mistake only surfaces much later. These hooks catch them at commit time.

Usage

Add the repo to your project's .pre-commit-config.yaml:

repos:
  - repo: https://github.com/specificlanguages/mps-pre-commit-hooks
    rev: v0.4.0
    hooks:
      - id: mps-check-orphan-modules
      - id: mps-check-unbuilt-modules
      - id: mps-check-missing-modules
      - id: mps-check-orphan-models
      - id: mps-check-orphan-mpsr-files
      - id: mps-check-well-formed-xml
      - id: mps-check-language-versions
      - id: mps-check-no-test-info
      - id: mps-check-banned-model-names
      - id: mps-check-module-naming
      - id: mps-check-model-naming
      - id: mps-check-path-variables

Then pre-commit install (or just prek install). The hooks work the same under both runners.

Each hook is independent — enable only the ones you want. They all scan the whole repository (not just the staged files), since most of what they check are cross-file relationships. The structural checks (orphans, and missing/dangling references) run on every commit, because the problem they catch is often introduced by a commit that only deletes a file — which pre-commit would otherwise skip them for. The per-file content checks (well-formed-xml, naming, path variables) run only when a relevant file is added or changed.

Hooks

mps-check-orphan-modules

Reports orphan modules*.msd / *.mpl / *.devkit / *.mpst files present on disk but not registered in any .mps/modules.xml. MPS opens a project from its modules.xml, so it silently ignores an unregistered module, and the mistake otherwise surfaces only much later.

mps-check-unbuilt-modules

Reports unbuilt modules — modules that are not mentioned in any MPS build script.

Exclude demo and sandbox modules with --exclude:

- id: mps-check-unbuilt-modules
  args: [--exclude=/code/applications, --exclude=_spreferences, --exclude=*.sandbox.msd]

mps-check-missing-modules / mps-fix-missing-modules

The reverse of the orphan-modules check: reports .mps/modules.xml entries whose modulePath points to a file that no longer exists on disk (typically left behind when a module is moved or deleted). Entries addressed through a path variable are ignored.

mps-check-missing-modules is read-only — it reports and fails. mps-fix-missing-modules removes the dangling <modulePath> entries from their modules.xml; use it when you want the hook to repair them for you:

- id: mps-fix-missing-modules
  args: [--exclude=examples/]

Both hooks accept repeatable --exclude options. Each is a .gitignore-style glob matched against the repository-relative .mps/modules.xml path. Use it when a project contains modulePath entries for modules that are not present on disk but should be ignored; MPS silently ignores those entries when loading the project. A trailing / excludes a whole subtree; for example, --exclude=examples/ skips every modules.xml below examples.

These are defined as separate hooks because the check hook may run in parallel with other read-only hooks whereas the fix hook requires serial execution (require_serial: true).

mps-check-orphan-models

Reports model files (*.mps / *.mpsr / .model) living outside every source root of every declared default model root. A model that falls under none of them is invisible to MPS. Such models may appear in the repository during merge conflict resolution.

mps-check-orphan-mpsr-files

Reports *.mpsr files whose directory has no .model header file alongside them. The header describes the model; without it MPS cannot load the roots, so the model is effectively lost.

mps-check-well-formed-xml

Reports MPS XML files that do not parse as well-formed XML — model files (*.mps / *.mpsr / .model), module descriptors (*.msd / *.mpl / *.devkit / *.mpst), and the per-project .mps/modules.xml / .mps/libraries.xml. Besides the zero-byte file left by a botched save, merge, or checkout, this catches a truncated file or one still carrying Git conflict markers — none of which MPS can load.

This is a thin wrapper over the check-xml hook from pre-commit/pre-commit-hooks, pre-pointed at MPS's XML file extensions so it needs no configuration. If you would rather wire up check-xml yourself — or already depend on that repo — enable it directly instead and give it the same file pattern:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v6.0.0
  hooks:
    - id: check-xml
      files: '(\.(msd|mpl|devkit|mps|mpsr|model)$)|((^|/)\.mps/(modules|libraries)\.xml$)'
      types: [file] # override the hook's default types: [xml], which excludes MPS's extensions

Wiring it up this way also lets prek substitute its built-in Rust check-xml, which it does automatically for the pre-commit/pre-commit-hooks repo — roughly twice as fast on a large repository. It does not do this for the wrapper above, whose fast path prek can't recognize. Under pre-commit the same config just runs the Python check-xml, so the snippet works on both runners.

mps-check-language-versions

Reports two problems in a model file's (*.mps / .model) <languages> header:

  • A language used with version="-1". MPS writes -1 when it saves a model while the used language's version is unknown — typically because the language module was not on the path at save time. The model still loads, but the missing version is a latent inconsistency that resurfaces as spurious diffs or migration problems once the language is available again.
  • A language whose version disagrees with the version the owning module records for that language in its descriptor's <languageVersions>. MPS reports this at load time ("Migration assistant detected inconsistency in language versions"). It is reported rather than fixed: MPS changes a model's language version only by running the actual migration, which migrates the node content along with the number.

Because the second check reads both sides, the hook also runs on module files (*.msd / *.mpl / *.devkit / *.mpst): a passed module descriptor is expanded to the models it owns, so a version bump in a descriptor is checked against its models even when no model file changed in the commit.

mps-check-no-test-info

Reports model files (*.mps / *.mpsr) whose registry instantiates the jetbrains.mps.lang.test TestInfo concept. The concept is matched by its language and concept id, not its name.

mps-check-banned-model-names

Reports model files (*.mps / .model) whose qualified name is one you forbid with --ban, a repeatable exact match against the full model name. The name is read from the model's ref header, so a model is reported through its .mps or .model file regardless of persistence format.

The motivating case is a generator model left with MPS' default unqualified name, main@generator: banning it catches that mistake while leaving a properly namespaced foo.bar.main@generator alone.

- id: mps-check-banned-model-names
  args: [--ban=main@generator]

mps-check-module-naming

Checks that every module descriptor (*.msd / *.mpl / *.devkit / *.mpst) agrees with its layout on disk. The directory and the file must be named after the full module name: com.example.foo must be located in com.example.foo/com.example.foo.mpl (likewise for other module types).

For language modules, nested runtime and sandbox modules can optionally use the layout foo.bar/runtime/foo.bar.runtime.msd and foo.bar/sandbox/foo.bar.sandbox.msd for the module names foo.bar.runtime and foo.bar.sandbox. Enable these exceptions explicitly; the language directory must contain a .mpl descriptor:

- id: mps-check-module-naming
  args: [--allow-nested-runtime, --allow-nested-sandbox]

Modules can be excluded with --exclude, a repeatable glob written like a .gitignore pattern:

- id: mps-check-module-naming
  args: [--exclude=_spreferences, --exclude=some.lang/sandbox]

mps-check-model-naming

Checks that model files (*.mps / .model) are named consistently with the model's name. What it checks depends on where the model lives.

For a model in a solution's or language's own model root, the file name must match the model name. Relative to the source root it lives under, a model named foo.bar.baz.quux in a module foo.bar may be stored as:

foo.bar.baz.quux.mps          the full name
baz.quux.mps                  the full name with the module name truncated away
foo/bar/baz/quux.mps          the full name, each segment its own directory
baz/quux.mps                  truncated, each segment its own directory

and any mix of dot- and directory-separated segments. Truncation drops the whole owning module name and nothing less, so bar.baz.quux.mps (only part of the module name removed) and an unrelated somethingElse.mps are both reported. A model in the per-root format is a directory of the same name holding a .model header, so the rules apply to the directory the .model sits in. A @stereotype (e.g. @tests) is part of the name and so of the file name — foo and foo@tests are different models, kept in different files.

The truncated short name is accepted by default. To require solution models to carry the full name, pass --no-short-names. Language models keep the short form regardless — the terse per-aspect file name (behavior.mps for com.example.lang.behavior) is the idiomatic language layout.

- id: mps-check-model-naming
  args: [--no-short-names]

For a model in a language's embedded generator, only the name is checked: it must be namespaced under the owning language (a model of language foo.bar must be named foo.bar…). This catches a template model left over from another language, or one left with a non-unique name like main@generator. The file layout of generator models is not checked — the generator/template folder makes it unpredictable, and the generator's own declared namespace is unreliable (legacy #id forms), so the language namespace is what a generator model is measured against.

A model that lies outside every declared source root is left to mps-check-orphan-models.

mps-check-path-variables / mps-fix-path-variables

Reports path variables in .mps/libraries.xml and .mps/modules.xml. Replacing these paths with project-relative paths makes it easier to check out and open the project.

mps-check-path-variables is check-only, it fails if a path variable is found. mps-fix-path-variables rewrites the offending paths in place; use it when you want the hook to repair them for you:

- id: mps-fix-path-variables

These are defined as separate hooks because the check hook may run in parallel with other read-only hooks whereas the fix hook requires serial execution (require_serial: true).

The fix assumes the macro's value is the Git repository root and re-expresses the whole path relative to $PROJECT_DIR$:

${mbeddr.github.core.home}/code/platform/com.mbeddr.doc
→ $PROJECT_DIR$/../../platform/com.mbeddr.doc

Caveat. This assumption holds for the common case where a variable points at the project's own checkout root. In other cases the rewritten path will be incorrect. Like other pre-commit fixers, the hook exits non-zero when it changes files, so you re-stage them deliberately.

License

MIT. See LICENSE.

About

Pre-commit hooks for JetBrains MPS projects

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages