Repository: gleam-lang/gleam (rust)

Issue:
OTP27 export moduledoc and doc for module docs and function docs
> 

This EEP proposes two new attributes: -doc and -moduledoc. They could be used as follows:

See: https://www.erlang.org/eeps/eep-0059#new-module-attributes

Implemented in OTP 27 https://www.erlang.org/news/167#documentation

Maybe `moduledoc false` for `@internal`? https://www.erlang.org/eeps/eep-0059#hidden-docs

Pull request description:
Include doc comments in generated Erlang code
This PR fixes #3010!

Function and module doc comments are includ
ed in the generated Erlang code, so one can now browse Gleam's documentation from the Erlang shell, like they would with any other Erlang module:

![Screenshot 2025-01-05 alle 14 47 39](https://github.com/user-attachments/assets/1786427f-343c-4b2d-b40c-69d637e0bef8)
![Screenshot 2025-01-05 alle 14 47 52](https://github.com/user-attachments/assets/7e3f1a01-1d43-4bbf-89cf-488f1b8c3896)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cfe8d2f1..94a5ef258 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@
 - Improved code generation for blocks in tail position on the Javascript target.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- Function documentation comments and module documentation comments are now
+  included in the generated Erlang code and can be browsed from the Erlang
+  shell starting from OTP27.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Build tool
 
 ### Language server
diff --git a/compiler-core/src/build/package_compiler.rs b/compiler-core/src/build/package_compiler.rs
index d57aa409b..9c0eed8bd 100644
--- a/compiler-core/src/build/package_compiler.rs
+++ b/compiler-core/src/build/package_compiler.rs
@@ -182,11 +182,15 @@ where
             incomplete_modules,
         );
 
-        let modules = match outcome {
+        let mut modules = match outcome {
             Outcome::Ok(modules) => modules,
             Outcome::PartialFailure(_, _) | Outcome::TotalFailure(_) => return outcome,
         };
 
+        for mut module in modules.iter_mut() {
+            module.attach_doc_and_module_comments();
+        }
+
         tracing::debug!("performing_code_generation");
 
         if let Err(error) = self.perform_codegen(&modules) {
diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs
index 73134cfd9..2a93565de 100644
--- a/compiler-core/src/erlang.rs
+++ b/compiler-core/src/erlang.rs
@@ -46,6 +46,7 @@ struct Env<'a> {
     module: &'a str,
     function: &'a str,
     line_numbers: &'a LineNumbers,
+    needs_function_docs: bool,
     current_scope_vars: im::HashMap<String, usize>,
     erl_function_scope_vars: im::HashMap<String, usize>,
 }
@@ -56,6 +57,7 @@ impl<'env> Env<'env> {
         Self {
             current_scope_vars: vars.clone(),
             erl_function_scope_vars: vars,
+            needs_function_docs: false,
             line_numbers,
             function,
             module,
@@ -217,21 +219,53 @@ fn module_document<'a>(
 
     let src_path = EcoString::from(module.type_info.src_path.as_str());
 
-    let statements = join(
-        module
-            .definitions
-            .iter()
-            .flat_map(|s| module_statement(s, &module.name, line_numbers, &src_path)),
+    let mut needs_function_docs = false;
+    let mut statements = Vec::with_capacity(module.definitions.len());
+    for definition in module.definitions.iter() {
+        if let Some((statement_document, env)) =
+            module_statement(definition, &module.name, line_numbers, &src_path)
+        {
+            needs_function_docs = needs_function_docs || env.needs_function_docs;
+            statements.push(statement_document);
+        }
+    }
+
+    // We're going to need the documentation directives if any of the module's
+    // functions need it, or if the module has a module comment that we want to
+    // include in the generated Erlang source.
+    let needs_function_docs = needs_function_docs || !module.documentation.is_empty();
+    let documentation_directive = if needs_function_docs {
+        "-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif."
+            .to_doc()
+            .append(lines(2))
+    } else {
+        nil()
+    };
+
+    let module_doc = if module.documentation.is_empty() {
+        nil()
+    } else {
+        doc_attribute(DocCommentKind::Module, &module.documentation).append(lines(2))
+    };
+
+    let module = docvec![
+        header,
+        "-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).",
         lines(2),
-    );
+        exports,
+        documentation_directive,
+        module_doc,
+        type_defs,
+        join(statements, lines(2)),
+    ];
 
-    Ok(header
-        .append("-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).")
-        .append(lines(2))
-        .append(exports)
-        .append(type_defs)
-        .append(statements)
-        .append(line()))
+    Ok(module.append(line()))
 }
 
 fn register_imports(
@@ -354,7 +388,7 @@ fn module_statement<'a>(
     module: &'a str,
     line_numbers: &'a LineNumbers,
     src_path: &EcoString,
-) -> Option<Document<'a>> {
+) -> Option<(Document<'a>, Env<'a>)> {
     match statement {
         Definition::TypeAlias(TypeAlias { .. })
         | Definition::CustomType(CustomType { .. })
@@ -372,7 +406,7 @@ fn module_function<'a>(
     module: &'a str,
     line_numbers: &'a LineNumbers,
     src_path: EcoString,
-) -> Option<Document<'a>> {
+) -> Option<(Document<'a>, Env<'a>)> {
     // Private external functions don't need to render anything, the underlying
     // Erlang implementation is used directly at the call site.
     if function.external_erlang.is_some() && function.publicity.is_private() {
@@ -424,16 +458,35 @@ fn module_function<'a>(
         })
         .unwrap_or_else(|| statement_sequence(&function.body, &mut env));
 
-    Some(docvec![
-        file_attribute,
-        line(),
-        spec,
-        atom_string(escape_erlang_existing_name(function_name).to_string()),
-        arguments,
-        " ->",
-        line().append(body).nest(INDENT).group(),
-        ".",
-    ])
+    let attributes = file_attribute;
+    let attributes = if let Some((_, documentation)) = &function.documentation {
+        env.needs_function_docs = true;
+        let doc_lines = documentation
+            .trim_end()
+            .split('\n')
+            .map(EcoString::from)
+            .collect_vec();
+
+        attributes
+            .append(line())
+            .append(doc_attribute(DocCommentKind::Function, &doc_lines))
+    } else {
+        attributes
+    };
+
+    Some((
+        docvec![
+            attributes,
+            line(),
+            spec,
+            atom_string(escape_erlang_existing_name(function_name).to_string()),
+            arguments,
+            " ->",
+            line().append(body).nest(INDENT).group(),
+            ".",
+        ],
+        env,
+    ))
 }
 
 fn file_attribute<'a>(
@@ -446,6 +499,32 @@ fn file_attribute<'a>(
     docvec!["-file(\"", path, "\", ", line, ")."]
 }
 
+enum DocCommentKind {
+    Module,
+    Function,
+}
+
+fn doc_attribute<'a>(kind: DocCommentKind, doc_lines: &[EcoString]) -> Document<'a> {
+    let prefix = match kind {
+        DocCommentKind::Module => "?MODULEDOC",
+        DocCommentKind::Function => "?DOC",
+    };
+    let is_multiline_doc_comment = doc_lines.len() > 1;
+    let doc_lines = join(
+        doc_lines.iter().map(|line| {
+            let line = line.replace("\\", "\\\\").replace("\"", "\\\"");
+            docvec!["\"", line, "\\n\""]
+        }),
+        line(),
+    );
+    if is_multiline_doc_comment {
+        let nested_documentation = docvec![line(), doc_lines].nest(INDENT);
+        docvec![prefix, "(", nested_documentation, line(), ")."]
+    } else {
+        docvec![prefix, "(", doc_lines, ")."]
+    }
+}
+
 fn external_fun_args<'a>(args: &'a [TypedArg], env: &mut Env<'a>) -> Document<'a> {
     wrap_args(args.iter().map(|a| {
         let name = match &a.names {


Test patch:
diff --git a/compiler-core/src/erlang/tests.rs b/compiler-core/src/erlang/tests.rs
index c7fe9417a..e5e0a328a 100644
--- a/compiler-core/src/erlang/tests.rs
+++ b/compiler-core/src/erlang/tests.rs
@@ -1,6 +1,9 @@
+use std::time::SystemTime;
+
 use camino::Utf8PathBuf;
 
 use crate::analyse::TargetSupport;
+use crate::build;
 use crate::config::PackageConfig;
 use crate::type_::PRELUDE_MODULE_NAME;
 use crate::warning::WarningEmitter;
@@ -17,6 +20,7 @@ mod case;
 mod conditional_compilation;
 mod consts;
 mod custom_types;
+mod documentation;
 mod external_fn;
 mod functions;
 mod guards;
@@ -91,10 +95,27 @@ pub fn compile_test_project(src: &str, src_path: &str, dep: Option<(&str, &str,
         target_support: TargetSupport::NotEnforced,
         package_config: &config,
     }
-    .infer_module(ast, line_numbers, path)
+    .infer_module(ast, line_numbers, path.clone())
     .expect("should successfully infer root Erlang");
+
+    // After building everything we still need to attach the module comments, to
+    // do that we're reusing the `attach_doc_and_module_comments` that's used
+    // for the real thing. We just have to make a placeholder module wrapping
+    // the parsed ast and call the function!
+    let mut built_module = build::Module {
+        name: "my/mod".into(),
+        code: src.into(),
+        mtime: SystemTime::UNIX_EPOCH,
+        input_path: path,
+        origin: Origin::Src,
+        ast,
+        extra: parsed.extra,
+        dependencies: vec![],
+    };
+    built_module.attach_doc_and_module_comments();
+
     let line_numbers = LineNumbers::new(src);
-    module(&ast, &line_numbers).unwrap()
+    module(&built_module.ast, &line_numbers).unwrap()
 }
 
 #[macro_export]
diff --git a/compiler-core/src/erlang/tests/documentation.rs b/compiler-core/src/erlang/tests/documentation.rs
new file mode 100644
index 000000000..66772fc5b
--- /dev/null
+++ b/compiler-core/src/erlang/tests/documentation.rs
@@ -0,0 +1,81 @@
+use crate::assert_erl;
+
+#[test]
+fn function_with_documentation() {
+    assert_erl!(
+        r#"
+/// Function doc!
+pub fn documented() { 1 }"#
+    );
+}
+
+#[test]
+fn function_with_multiline_documentation() {
+    assert_erl!(
+        r#"
+/// Function doc!
+/// Hello!!
+///
+pub fn documented() { 1 }"#
+    );
+}
+
+#[test]
+fn quotes_in_documentation_are_escaped() {
+    assert_erl!(
+        r#"
+/// "hello"
+pub fn documented() { 1 }"#
+    );
+}
+
+#[test]
+fn backslashes_in_documentation_are_escaped() {
+    assert_erl!(
+        r#"
+/// \hello\
+pub fn documented() { 1 }"#
+    );
+}
+
+#[test]
+fn single_line_module_comment() {
+    assert_erl!(
+        r#"
+//// Hello! This is a single line module comment.
+
+pub fn main() { 1 }"#
+    );
+}
+
+#[test]
+fn multi_line_module_comment() {
+    assert_erl!(
+        r#"
+//// Hello! This is a multi-
+//// line module comment.
+////
+
+pub fn main() { 1 }"#
+    );
+}
+
+#[test]
+fn double_quotes_are_escaped_in_module_comment() {
+    assert_erl!(
+        r#"
+//// "quotes!"
+
+pub fn main() { 1 }"#
+    );
+}
+
+#[test]
+fn backslashes_are_escaped_in_module_comment() {
+    assert_erl!(
+        r#"
+//// \backslashes!\
+
+pub fn main() { 1 }"#
+    );
+}
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_are_escaped_in_module_comment.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_are_escaped_in_module_comment.snap
new file mode 100644
index 000000000..507b51926
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_are_escaped_in_module_comment.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n//// \\backslashes!\\\n\npub fn main() { 1 }"
+---
+----- SOURCE CODE
+
+//// \backslashes!\
+
+pub fn main() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+?MODULEDOC(" \\backslashes!\\\n").
+
+-file("/root/project/test/my/mod.gleam", 4).
+-spec main() -> integer().
+main() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_in_documentation_are_escaped.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_in_documentation_are_escaped.snap
new file mode 100644
index 000000000..48065cd78
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__backslashes_in_documentation_are_escaped.snap
@@ -0,0 +1,28 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n/// \\hello\\\npub fn documented() { 1 }"
+---
+----- SOURCE CODE
+
+/// \hello\
+pub fn documented() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([documented/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+-file("/root/project/test/my/mod.gleam", 3).
+?DOC(" \\hello\\\n").
+-spec documented() -> integer().
+documented() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__double_quotes_are_escaped_in_module_comment.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__double_quotes_are_escaped_in_module_comment.snap
new file mode 100644
index 000000000..5833aaeee
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__double_quotes_are_escaped_in_module_comment.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n//// \"quotes!\"\n\npub fn main() { 1 }"
+---
+----- SOURCE CODE
+
+//// "quotes!"
+
+pub fn main() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+?MODULEDOC(" \"quotes!\"\n").
+
+-file("/root/project/test/my/mod.gleam", 4).
+-spec main() -> integer().
+main() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_documentation.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_documentation.snap
new file mode 100644
index 000000000..218f9960a
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_documentation.snap
@@ -0,0 +1,28 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n/// Function doc!\npub fn documented() { 1 }"
+---
+----- SOURCE CODE
+
+/// Function doc!
+pub fn documented() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([documented/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+-file("/root/project/test/my/mod.gleam", 3).
+?DOC(" Function doc!\n").
+-spec documented() -> integer().
+documented() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_multiline_documentation.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_multiline_documentation.snap
new file mode 100644
index 000000000..6be7d5b24
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__function_with_multiline_documentation.snap
@@ -0,0 +1,33 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n/// Function doc!\n/// Hello!!\n///\npub fn documented() { 1 }"
+---
+----- SOURCE CODE
+
+/// Function doc!
+/// Hello!!
+///
+pub fn documented() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([documented/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+-file("/root/project/test/my/mod.gleam", 5).
+?DOC(
+    " Function doc!\n"
+    " Hello!!\n"
+).
+-spec documented() -> integer().
+documented() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__multi_line_module_comment.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__multi_line_module_comment.snap
new file mode 100644
index 000000000..78bb5e193
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__multi_line_module_comment.snap
@@ -0,0 +1,36 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n//// Hello! This is a multi-\n//// line module comment.\n////\n\npub fn main() { 1 }"
+---
+----- SOURCE CODE
+
+//// Hello! This is a multi-
+//// line module comment.
+////
+
+pub fn main() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+?MODULEDOC(
+    " Hello! This is a multi-\n"
+    " line module comment.\n"
+    "\n"
+).
+
+-file("/root/project/test/my/mod.gleam", 6).
+-spec main() -> integer().
+main() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__quotes_in_documentation_are_escaped.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__quotes_in_documentation_are_escaped.snap
new file mode 100644
index 000000000..486a4e866
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__quotes_in_documentation_are_escaped.snap
@@ -0,0 +1,28 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n/// \"hello\"\npub fn documented() { 1 }"
+---
+----- SOURCE CODE
+
+/// "hello"
+pub fn documented() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([documented/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+-file("/root/project/test/my/mod.gleam", 3).
+?DOC(" \"hello\"\n").
+-spec documented() -> integer().
+documented() ->
+    1.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__single_line_module_comment.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__single_line_module_comment.snap
new file mode 100644
index 000000000..d5e86a1e3
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__documentation__single_line_module_comment.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/erlang/tests/documentation.rs
+expression: "\n//// Hello! This is a single line module comment.\n\npub fn main() { 1 }"
+---
+----- SOURCE CODE
+
+//// Hello! This is a single line module comment.
+
+pub fn main() { 1 }
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+
+-export([main/0]).
+
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+?MODULEDOC(" Hello! This is a single line module comment.\n").
+
+-file("/root/project/test/my/mod.gleam", 4).
+-spec main() -> integer().
+main() ->
+    1.
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_function_definition_with_docs.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_function_definition_with_docs.snap
index 2cc8a9f13..8f699f6d1 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_function_definition_with_docs.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_function_definition_with_docs.snap
@@ -13,6 +13,6 @@ fn append(x, y) {
 ----- Hover content -----
 Scalar(
     String(
-        "```gleam\nfn(String, String) -> String\n```\n Exciting documentation\n Maybe even multiple lines\n",
+        "```gleam\nfn(String, String) -> String\n```\n Exciting documentation\n Maybe even multiple lines",
     ),
 )
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_module_constant.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_module_constant.snap
index be57c67f1..8c1170d78 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_module_constant.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_module_constant.snap
@@ -11,6 +11,6 @@ const one = 1
 ----- Hover content -----
 Scalar(
     String(
-        "```gleam\nInt\n```\n Exciting documentation\n Maybe even multiple lines\n",
+        "```gleam\nInt\n```\n Exciting documentation\n Maybe even multiple lines",
     ),
 )
diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__import_shadowed_name_warning.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__import_shadowed_name_warning.snap
index d11b9f3c9..196d5b82a 100644
--- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__import_shadowed_name_warning.snap
+++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__import_shadowed_name_warning.snap
@@ -1,6 +1,5 @@
 ---
 source: test-package-compiler/src/generated_tests.rs
-assertion_line: 151
 expression: "./cases/import_shadowed_name_warning"
 ---
 //// /out/lib/the_package/_gleam_artefacts/one.cache
@@ -33,6 +32,16 @@ expression: "./cases/import_shadowed_name_warning"
 -export([use_type/1]).
 -export_type([shadowing/0]).
 
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
+?MODULEDOC(" https://github.com/gleam-lang/otp/pull/22\n").
+
 -type shadowing() :: port.
 
 -file("src/two.gleam", 14).
diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_constants.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_constants.snap
index 7e7558517..c1b1e09d8 100644
--- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_constants.snap
+++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_constants.snap
@@ -1,6 +1,5 @@
 ---
 source: test-package-compiler/src/generated_tests.rs
-assertion_line: 162
 expression: "./cases/imported_constants"
 ---
 //// /out/lib/the_package/_gleam_artefacts/one.cache
@@ -36,7 +35,19 @@ expression: "./cases/imported_constants"
 
 -export([accessors/1, destructure_qualified/1, destructure_unqualified/1, destructure_aliased/1, qualified_fn_a/0, qualified_fn_b/0, unqualified_fn_a/0, unqualified_fn_b/0, aliased_fn_a/0, aliased_fn_b/0]).
 
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
 -file("src/two.gleam", 45).
+?DOC(
+    " For these statements we use the accessors for the record from the other\n"
+    " module\n"
+).
 -spec accessors(one:user()) -> {binary(), integer()}.
 accessors(User) ->
     Name = erlang:element(2, User),
@@ -44,6 +55,7 @@ accessors(User) ->
     {Name, Score}.
 
 -file("src/two.gleam", 52).
+?DOC(" For these statements we use destructure the record\n").
 -spec destructure_qualified(one:user()) -> {binary(), integer()}.
 destructure_qualified(User) ->
     {user, Name, Score} = User,
diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_record_constructors.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_record_constructors.snap
index 53cd1120d..cd98a071c 100644
--- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_record_constructors.snap
+++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__imported_record_constructors.snap
@@ -35,7 +35,19 @@ expression: "./cases/imported_record_constructors"
 
 -export([accessors/1, destructure_qualified/1, destructure_qualified_aliased/1, destructure_unqualified/1, destructure_aliased/1, update_qualified/1, update_qualified_aliased/1, update_unqualified/1, update_aliased/1, qualified_fn_a/0, qualified_fn_b/0, qualified_aliased_fn_a/0, qualified_aliased_fn_b/0, unqualified_fn_a/0, unqualified_fn_b/0, aliased_fn_a/0, aliased_fn_b/0]).
 
+-if(?OTP_RELEASE >= 27).
+-define(MODULEDOC(Str), -moduledoc(Str)).
+-define(DOC(Str), -doc(Str)).
+-else.
+-define(MODULEDOC(Str), -compile([])).
+-define(DOC(Str), -compile([])).
+-endif.
+
 -file("src/two.gleam", 58).
+?DOC(
+    " For these statements we use the accessors for the record from the other\n"
+    " module\n"
+).
 -spec accessors(one@one:user()) -> {binary(), integer()}.
 accessors(User) ->
     Name = erlang:element(2, User),
@@ -43,6 +55,7 @@ accessors(User) ->
     {Name, Score}.
 
 -file("src/two.gleam", 65).
+?DOC(" For these statements we use destructure the record\n").
 -spec destructure_qualified(one@one:user()) -> {binary(), integer()}.
 destructure_qualified(User) ->
     {user, Name, Score} = User,
@@ -67,6 +80,7 @@ destructure_aliased(User) ->
     {Name, Score}.
 
 -file("src/two.gleam", 86).
+?DOC(" For these statements we use update the record\n").
 -spec update_qualified(one@one:user()) -> one@one:user().
 update_qualified(User) ->
     _record = User,