diff --git a/src/main/resources/org/eolang/lints/misc/roll-bases.xsl b/src/main/resources/org/eolang/lints/misc/roll-bases.xsl
new file mode 100644
index 000000000..a32456d3c
--- /dev/null
+++ b/src/main/resources/org/eolang/lints/misc/roll-bases.xsl
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ warning
+ The base "
+
+ " must be written instead of the nested bases "
+
+ " and "
+
+ "
+
+
+
+
+
diff --git a/src/main/resources/org/eolang/motives/misc/roll-bases.md b/src/main/resources/org/eolang/motives/misc/roll-bases.md
new file mode 100644
index 000000000..4262a6123
--- /dev/null
+++ b/src/main/resources/org/eolang/motives/misc/roll-bases.md
@@ -0,0 +1,24 @@
+# Roll bases
+
+A composite `@base` attribute, written as nested objects, must be
+collapsed into a single short form when possible. For example:
+
+```xml
+
+
+
+
+
+```
+
+must be written as:
+
+```xml
+
+```
+
+The collapse is possible when the outer object has a `@base` starting
+with `.`, and its single child has a `@base` that does not start with
+`.`, has no inner objects, no data, and no name. Such unrolled bases
+are usually a result of hand-written XMIR or of XMIR produced by tools
+other than `eo-parser`, which already rolls bases during parsing.
diff --git a/src/test/java/org/eolang/lints/LtByXslTest.java b/src/test/java/org/eolang/lints/LtByXslTest.java
index 144bef92c..031f3fb45 100644
--- a/src/test/java/org/eolang/lints/LtByXslTest.java
+++ b/src/test/java/org/eolang/lints/LtByXslTest.java
@@ -404,6 +404,76 @@ void validatesEoPacksForErrors() throws IOException {
);
}
+ @Test
+ void catchesUnrolledBases() throws ImpossibleModificationException {
+ MatcherAssert.assertThat(
+ "An unrolled composite base must be reported",
+ new LtByXsl("misc/roll-bases").defects(
+ new XMLDocument(
+ new Xembler(
+ new Directives().add("object")
+ .add("o").attr("base", ".foo")
+ .add("o").attr("base", "x").up()
+ .up().up()
+ ).xml()
+ )
+ ),
+ Matchers.iterableWithSize(1)
+ );
+ }
+
+ @Test
+ void allowsRolledBase() throws ImpossibleModificationException {
+ MatcherAssert.assertThat(
+ "A rolled composite base must not be reported",
+ new LtByXsl("misc/roll-bases").defects(
+ new XMLDocument(
+ new Xembler(
+ new Directives().add("object")
+ .add("o").attr("base", "x.foo").up().up()
+ ).xml()
+ )
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Test
+ void allowsNestedBaseWithData() throws ImpossibleModificationException {
+ MatcherAssert.assertThat(
+ "A child with data must not be rolled",
+ new LtByXsl("misc/roll-bases").defects(
+ new XMLDocument(
+ new Xembler(
+ new Directives().add("object")
+ .add("o").attr("base", ".foo")
+ .add("o").attr("base", "x").set("data").up()
+ .up().up()
+ ).xml()
+ )
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Test
+ void allowsDoubleDotBases() throws ImpossibleModificationException {
+ MatcherAssert.assertThat(
+ "Two dot-prefixed bases must not be rolled",
+ new LtByXsl("misc/roll-bases").defects(
+ new XMLDocument(
+ new Xembler(
+ new Directives().add("object")
+ .add("o").attr("base", ".foo")
+ .add("o").attr("base", ".bar").up()
+ .up().up()
+ ).xml()
+ )
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
/**
* Whether a pack is eligible for validation?
* @param pack Pack