Repository: pylint-dev/pylint

Issue:
Mermaid printer renders double underscores as bold formatting instead of literal underscores
The Mermaid printer in pyreverse incorrectly renders Python dunder methods and attributes. Double underscores (`__`) are output directly to Mermaid, which interprets them as Markdown bold formatting (`**text**`) instead of literal underscores.

This can be observed in the [functional test files](https://github.com/pylint-dev/pylint/blob/main/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd)

The file contains entries like:
```
class P {
  name : str
  __init__(name: str)
}
class SpecialAttr {
  __x__
  __init__()
}
```

When viewed in a Mermaid renderer, `__init__` and `__x__` display as bold text instead of showing the literal underscores.

**Command used:**
```bash
pyreverse -o mmd --filter-mode=ALL tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.py
```

**Pylint output:**
The generated `.mmd` file contains unescaped double underscores that render incorrectly in Mermaid viewers.

**Expected behavior:**
Double underscores in method and attribute names should render as literal underscores in Mermaid diagrams, not as bold formatting.

**Pylint version:**
```
pylint 4.0.0-dev0
astroid 4.0.0a0
Python 3.13.2 (main, Feb 12 2025, 14:49:53) [MSC v.1942 64 bit (AMD64)]
```


Discussion:

Patch:
diff --git a/doc/whatsnew/fragments/10402.bugfix b/doc/whatsnew/fragments/10402.bugfix
new file mode 100644
index 0000000000..a7cea6782e
--- /dev/null
+++ b/doc/whatsnew/fragments/10402.bugfix
@@ -0,0 +1,3 @@
+Fix double underscores erroneously rendering as bold in pyreverse's Mermaid output.
+
+Closes #10402
diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py
index 22a8f768b1..0f1ebd04f0 100644
--- a/pylint/pyreverse/mermaidjs_printer.py
+++ b/pylint/pyreverse/mermaidjs_printer.py
@@ -32,6 +32,11 @@ def _open_graph(self) -> None:
         self.emit("classDiagram")
         self._inc_indent()
 
+    def _escape_mermaid_text(self, text: str) -> str:
+        """Escape characters that conflict with Markdown formatting."""
+        text = text.replace("__", r"\_\_")  # Double underscore → escaped
+        return text
+
     def emit_node(
         self,
         name: str,
@@ -48,14 +53,24 @@ def emit_node(
         nodetype = self.NODES[type_]
         body = []
         if properties.attrs:
-            body.extend(properties.attrs)
+            # Escape attribute names to prevent Markdown formatting issues
+            escaped_attrs = [
+                self._escape_mermaid_text(attr) for attr in properties.attrs
+            ]
+            body.extend(escaped_attrs)
         if properties.methods:
             for func in properties.methods:
                 args = self._get_method_arguments(func)
-                line = f"{func.name}({', '.join(args)})"
+                # Escape method name and arguments
+                escaped_method_name = self._escape_mermaid_text(func.name)
+                escaped_args = [self._escape_mermaid_text(arg) for arg in args]
+                line = f"{escaped_method_name}({', '.join(escaped_args)})"
                 line += "*" if func.is_abstract() else ""
                 if func.returns:
-                    line += f" {get_annotation_label(func.returns)}"
+                    # Escape return type annotation
+                    return_type = get_annotation_label(func.returns)
+                    escaped_return_type = self._escape_mermaid_text(return_type)
+                    line += f" {escaped_return_type}"
                 body.append(line)
         name = name.split(".")[-1]
         self.emit(f"{nodetype} {name} {{")
@@ -80,7 +95,7 @@ def emit_edge(
         to_node = to_node.split(".")[-1]
         edge = f"{from_node} {self.ARROWS[type_]} {to_node}"
         if label:
-            edge += f" : {label}"
+            edge += f" : {self._escape_mermaid_text(label)}"
         self.emit(edge)
 
     def _close_graph(self) -> None:


Test patch:
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd
index 3b445288a1..378ef5ccaf 100644
--- a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd
@@ -1,25 +1,25 @@
 classDiagram
   class P {
     name : str
-    __init__(name: str)
+    \_\_init\_\_(name: str)
   }
   class PrivateAttr {
-    __x
-    __init__()
+    \_\_x
+    \_\_init\_\_()
   }
   class ProtectedAttr {
     _x
-    __init__()
+    \_\_init\_\_()
   }
   class PublicAttr {
     x
-    __init__()
+    \_\_init\_\_()
   }
   class SpecialAttr {
-    __x__
-    __init__()
+    \_\_x\_\_
+    \_\_init\_\_()
   }
-  P --* PrivateAttr : __x
+  P --* PrivateAttr : \_\_x
   P --* ProtectedAttr : _x
   P --* PublicAttr : x
-  P --* SpecialAttr : __x__
+  P --* SpecialAttr : \_\_x\_\_
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd
index 6cfcae244f..07439b6c55 100644
--- a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd
@@ -1,21 +1,21 @@
 classDiagram
   class P {
     name : str
-    __init__(name: str)
+    \_\_init\_\_(name: str)
   }
   class PrivateAttr {
-    __init__()
+    \_\_init\_\_()
   }
   class ProtectedAttr {
-    __init__()
+    \_\_init\_\_()
   }
   class PublicAttr {
     x
-    __init__()
+    \_\_init\_\_()
   }
   class SpecialAttr {
-    __x__
-    __init__()
+    \_\_x\_\_
+    \_\_init\_\_()
   }
   P --* PublicAttr : x
-  P --* SpecialAttr : __x__
+  P --* SpecialAttr : \_\_x\_\_
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd
index 149b0a694a..6b7f39b905 100644
--- a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd
@@ -3,7 +3,7 @@ classDiagram
     name : str
   }
   class PrivateAttr {
-    __x
+    \_\_x
   }
   class ProtectedAttr {
     _x
@@ -13,6 +13,6 @@ classDiagram
   }
   class SpecialAttr {
   }
-  P --* PrivateAttr : __x
+  P --* PrivateAttr : \_\_x
   P --* ProtectedAttr : _x
   P --* PublicAttr : x