Fork of daniellemaywood.uk/gleam — Wasm codegen work
35 kB
1444 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Gleam contributors
3
4use std::{
5 collections::{HashMap, HashSet},
6 time::SystemTime,
7};
8
9use super::{
10 Dependency, DependencyKind, DocumentationConfig, SearchData, SearchItem, SearchItemType,
11 SearchProgrammingLanguage,
12 printer::{PrintOptions, Printer},
13 source_links::SourceLinker,
14};
15use crate::{
16 build::{
17 self, Mode, NullTelemetry, Origin, PackageCompiler, StaleTracker,
18 TargetCodegenConfiguration,
19 },
20 config::{DocsPage, PackageConfig, Repository},
21 docs::{DocContext, search_item_for_module, search_item_for_type, search_item_for_value},
22 io::{FileSystemWriter, memory::InMemoryFileSystem},
23 parse::extra::ModuleExtra,
24 paths::ProjectPaths,
25 type_,
26 uid::UniqueIdGenerator,
27 version::COMPILER_VERSION,
28 warning::WarningEmitter,
29};
30use camino::Utf8PathBuf;
31use ecow::{EcoString, eco_format};
32use hexpm::version::Version;
33use http::Uri;
34use itertools::Itertools;
35use serde_json::to_string as serde_to_string;
36
37#[derive(Default)]
38struct CompileWithMarkdownPagesOpts {
39 hex_publish: Option<DocContext>,
40}
41
42fn compile_with_markdown_pages(
43 config: PackageConfig,
44 modules: Vec<(&str, &str)>,
45 markdown_pages: Vec<(&str, &str)>,
46 opts: CompileWithMarkdownPagesOpts,
47) -> EcoString {
48 let fs = InMemoryFileSystem::new();
49 for (name, src) in modules {
50 fs.write(&Utf8PathBuf::from(format!("/src/{name}")), src)
51 .unwrap();
52 }
53
54 // We're saving the pages under a different `InMemoryFileSystem` for these
55 // tests so we don't have to juggle with borrows and lifetimes.
56 // The package compiler is going to take ownership of `fs` but later
57 // `generate_html` also needs a `FileSystemReader` to go and read the
58 // markdown pages' content.
59 let pages_fs = InMemoryFileSystem::new();
60 for (title, src) in markdown_pages.iter() {
61 pages_fs
62 .write(&Utf8PathBuf::from(format!("{title}.md")), src)
63 .unwrap();
64 }
65
66 let ids = UniqueIdGenerator::new();
67 let mut type_manifests = im::HashMap::new();
68 let mut defined_modules = im::HashMap::new();
69 let warnings = WarningEmitter::null();
70 let target = TargetCodegenConfiguration::Erlang { app_file: None };
71
72 let root = Utf8PathBuf::from("/");
73 let build = root.join("build");
74 let lib = root.join("lib");
75 let paths = ProjectPaths::new(root.clone());
76 let mut compiler =
77 PackageCompiler::new(&config, Mode::Dev, &root, &build, &lib, &target, ids, fs);
78 compiler.write_entrypoint = false;
79 compiler.write_metadata = false;
80 compiler.compile_beam_bytecode = true;
81 let mut modules = compiler
82 .compile(
83 &warnings,
84 &mut type_manifests,
85 &mut defined_modules,
86 &mut StaleTracker::default(),
87 &mut HashSet::new(),
88 &NullTelemetry,
89 )
90 .unwrap()
91 .modules;
92
93 for module in &mut modules {
94 module.attach_doc_and_module_comments();
95 }
96
97 let docs_pages = markdown_pages
98 .into_iter()
99 .map(|(title, _)| DocsPage {
100 title: (*title).into(),
101 path: Utf8PathBuf::from(format!("{title}.html")),
102 source: format!("{title}.md").into(),
103 })
104 .collect_vec();
105
106 super::generate_html(
107 &paths,
108 DocumentationConfig {
109 package_config: &config,
110 dependencies: HashMap::new(),
111 analysed: &modules,
112 docs_pages: &docs_pages,
113 rendering_timestamp: SystemTime::UNIX_EPOCH,
114 context: if let Some(doc_context) = opts.hex_publish {
115 doc_context
116 } else {
117 DocContext::HexPublish
118 },
119 },
120 pages_fs,
121 )
122 .into_iter()
123 .filter(|file| file.path.extension() == Some("html"))
124 .sorted_by(|a, b| a.path.cmp(&b.path))
125 .flat_map(|file| {
126 Some(format!(
127 "//// {}\n\n{}\n\n",
128 file.path.as_str(),
129 file.content
130 .text()?
131 .replace(COMPILER_VERSION, "GLEAM_VERSION_HERE")
132 ))
133 })
134 .collect::<String>()
135 .chars()
136 .collect()
137}
138
139pub fn compile(config: PackageConfig, modules: Vec<(&str, &str)>) -> EcoString {
140 compile_with_markdown_pages(
141 config,
142 modules,
143 vec![],
144 CompileWithMarkdownPagesOpts::default(),
145 )
146}
147
148fn compile_documentation(
149 module_name: &str,
150 module_src: &str,
151 modules: Vec<(&str, &str, &str)>,
152 dependency_kind: DependencyKind,
153 options: PrintOptions,
154) -> EcoString {
155 let module = type_::tests::compile_module(module_name, module_src, None, modules.clone())
156 .expect("Module should compile successfully");
157
158 let mut config = PackageConfig::default();
159 config.name = "thepackage".into();
160 let paths = ProjectPaths::new("/".into());
161 let build_module = build::Module {
162 name: "main".into(),
163 code: module_src.into(),
164 mtime: SystemTime::now(),
165 input_path: "/".into(),
166 origin: Origin::Src,
167 ast: module,
168 extra: ModuleExtra::new(),
169 dependencies: vec![],
170 };
171
172 let source_links = SourceLinker::new(&paths, &config, &build_module);
173
174 let module = build_module.ast;
175 let dependencies = modules
176 .iter()
177 .map(|(package, _, _)| {
178 (
179 EcoString::from(*package),
180 Dependency {
181 version: Version::new(1, 0, 0),
182 kind: dependency_kind,
183 },
184 )
185 })
186 .collect();
187
188 let mut printer = Printer::new(
189 module.type_info.package.clone(),
190 module.name.clone(),
191 &module.names,
192 &dependencies,
193 );
194 printer.set_options(options);
195
196 let types = printer.type_definitions(&source_links, &module.definitions);
197 let values = printer.value_definitions(&source_links, &module.definitions);
198
199 let mut output = EcoString::new();
200
201 output.push_str("---- SOURCE CODE\n");
202 for (_package, name, src) in modules {
203 output.push_str("-- ");
204 output.push_str(name);
205 output.push_str(".gleam\n");
206 output.push_str(src);
207 output.push_str("\n\n");
208 }
209 output.push_str("-- ");
210 output.push_str(module_name);
211 output.push_str(".gleam\n");
212 output.push_str(module_src);
213
214 if !types.is_empty() {
215 output.push_str("\n\n---- TYPES");
216 }
217 for type_ in types {
218 output.push_str("\n\n--- ");
219 output.push_str(type_.name);
220 if !type_.documentation.is_empty() {
221 output.push('\n');
222 output.push_str(&type_.documentation);
223 }
224 output.push_str("\n<pre><code>");
225 output.push_str(&type_.definition);
226 output.push_str("</code></pre>");
227
228 if !type_.constructors.is_empty() {
229 output.push_str("\n\n-- CONSTRUCTORS");
230 }
231 for constructor in type_.constructors {
232 output.push_str("\n\n");
233 if !constructor.documentation.is_empty() {
234 output.push_str(&constructor.documentation);
235 output.push('\n');
236 }
237 output.push_str("<pre><code>");
238 output.push_str(&constructor.definition);
239 output.push_str("</code></pre>");
240 }
241 }
242
243 if !values.is_empty() {
244 output.push_str("\n\n---- VALUES");
245 }
246 for value in values {
247 output.push_str("\n\n--- ");
248 output.push_str(value.name);
249 if !value.documentation.is_empty() {
250 output.push('\n');
251 output.push_str(&value.documentation);
252 }
253 output.push_str("\n<pre><code>");
254 output.push_str(&value.definition);
255 output.push_str("</code></pre>");
256 }
257
258 output
259}
260
261macro_rules! assert_documentation {
262 ($src:literal $(,)?) => {
263 assert_documentation!($src, PrintOptions::all());
264 };
265
266 ($src:literal, $options:expr $(,)?) => {
267 let output = compile_documentation("main", $src, Vec::new(), DependencyKind::Hex, $options);
268 insta::assert_snapshot!(output);
269 };
270
271 ($(($name:expr, $module_src:literal)),+, $src:literal $(,)?) => {
272 let output = compile_documentation(
273 "main",
274 $src,
275 vec![$(("thepackage", $name, $module_src)),*],
276 DependencyKind::Hex,
277 PrintOptions::all(),
278 );
279 insta::assert_snapshot!(output);
280 };
281
282 ($(($name:expr, $module_src:literal)),+, $src:literal, $options:expr $(,)?) => {
283 let output = compile_documentation(
284 "main",
285 $src,
286 vec![$(("thepackage", $name, $module_src)),*],
287 DependencyKind::Hex,
288 $options,
289 );
290 insta::assert_snapshot!(output);
291 };
292
293 ($(($name:expr, $module_src:literal)),+, $main_module:literal, $src:literal, $options:expr $(,)?) => {
294 let output = compile_documentation(
295 $main_module,
296 $src,
297 vec![$(("thepackage", $name, $module_src)),*],
298 DependencyKind::Hex,
299 $options,
300 );
301 insta::assert_snapshot!(output);
302 };
303
304 ($(($package:expr, $name:expr, $module_src:literal)),+, $src:literal $(,)?) => {
305 let output = compile_documentation(
306 "main",
307 $src,
308 vec![$(($package, $name, $module_src)),*],
309 DependencyKind::Hex,
310 PrintOptions::all(),
311 );
312 insta::assert_snapshot!(output);
313 };
314
315 ($(($package:expr, $name:expr, $module_src:literal)),+, $src:literal, $options:expr $(,)?) => {
316 let output = compile_documentation(
317 "main",
318 $src,
319 vec![$(($package, $name, $module_src)),*],
320 DependencyKind::Hex,
321 $options,
322 );
323 insta::assert_snapshot!(output);
324 };
325
326 (git: $(($package:expr, $name:expr, $module_src:literal)),+, $src:literal, $options:expr $(,)?) => {
327 let output = compile_documentation(
328 "main",
329 $src,
330 vec![$(($package, $name, $module_src)),*],
331 DependencyKind::Git,
332 $options,
333 );
334 insta::assert_snapshot!(output);
335 };
336
337 (path: $(($package:expr, $name:expr, $module_src:literal)),+, $src:literal, $options:expr $(,)?) => {
338 let output = compile_documentation(
339 "main",
340 $src,
341 vec![$(($package, $name, $module_src)),*],
342 DependencyKind::Path,
343 $options,
344 );
345 insta::assert_snapshot!(output);
346 };
347}
348
349#[test]
350fn hello_docs() {
351 let mut config = PackageConfig::default();
352 config.name = EcoString::from("test_project_name");
353 let modules = vec![(
354 "app.gleam",
355 r#"
356/// Here is some documentation
357pub fn one() {
358 1
359}
360"#,
361 )];
362 insta::assert_snapshot!(compile(config, modules));
363}
364
365#[test]
366fn ignored_argument_is_called_arg() {
367 let mut config = PackageConfig::default();
368 config.name = EcoString::from("test_project_name");
369 let modules = vec![("app.gleam", "pub fn one(_) { 1 }")];
370 insta::assert_snapshot!(compile(config, modules));
371}
372
373// https://github.com/gleam-lang/gleam/issues/2347
374#[test]
375fn tables() {
376 let mut config = PackageConfig::default();
377 config.name = EcoString::from("test_project_name");
378 let modules = vec![(
379 "app.gleam",
380 r#"
381/// | heading 1 | heading 2 |
382/// |--------------|--------------|
383/// | row 1 cell 1 | row 1 cell 2 |
384/// | row 2 cell 1 | row 2 cell 2 |
385///
386pub fn one() {
387 1
388}
389"#,
390 )];
391 insta::assert_snapshot!(compile(config, modules));
392}
393
394// https://github.com/gleam-lang/gleam/issues/2202
395#[test]
396fn long_function_wrapping() {
397 let mut config = PackageConfig::default();
398 config.name = EcoString::from("test_project_name");
399 let modules = vec![(
400 "app.gleam",
401 r#"
402pub type Option(t) {
403 Some(t)
404 None
405}
406
407/// Returns the first value if it is `Some`, otherwise evaluates the given
408/// function for a fallback value.
409///
410pub fn lazy_or(first: Option(a), second: fn() -> Option(a)) -> Option(a) {
411 case first {
412 Some(_) -> first
413 None -> second()
414 }
415}
416"#,
417 )];
418
419 insta::assert_snapshot!(compile(config, modules));
420}
421
422#[test]
423fn internal_definitions_are_not_included() {
424 let mut config = PackageConfig::default();
425 config.name = EcoString::from("test_project_name");
426 let modules = vec![(
427 "app.gleam",
428 r#"
429@internal
430pub const wibble = 1
431
432@internal
433pub type Wibble = Int
434
435@internal
436pub type Wobble { Wobble }
437
438@internal
439pub fn one() { 1 }
440"#,
441 )];
442 insta::assert_snapshot!(compile(config, modules));
443}
444
445// https://github.com/gleam-lang/gleam/issues/2561
446#[test]
447fn discarded_arguments_are_not_shown() {
448 let mut config = PackageConfig::default();
449 config.name = EcoString::from("test_project_name");
450 let modules = vec![("app.gleam", "pub fn discard(_discarded: a) -> Int { 1 }")];
451 insta::assert_snapshot!(compile(config, modules));
452}
453
454// https://github.com/gleam-lang/gleam/issues/2631
455#[test]
456fn docs_of_a_type_constructor_are_not_used_by_the_following_function() {
457 let mut config = PackageConfig::default();
458 config.name = EcoString::from("test_project_name");
459 let modules = vec![(
460 "app.gleam",
461 r#"
462pub type Wibble {
463 Wobble(
464 /// Documentation!!
465 wabble: Int,
466 )
467}
468
469pub fn main() { todo }
470"#,
471 )];
472 insta::assert_snapshot!(compile(config, modules));
473}
474
475#[test]
476fn markdown_code_from_standalone_pages_is_not_trimmed() {
477 let mut config = PackageConfig::default();
478 config.name = EcoString::from("test_project_name");
479 let pages = vec![(
480 "one",
481 "
482This is an example code snippet that should be indented
483```gleam
484pub fn indentation_test() {
485 todo as \"This line should be indented by two spaces\"
486}
487```",
488 )];
489 insta::assert_snapshot!(compile_with_markdown_pages(
490 config,
491 vec![],
492 pages,
493 CompileWithMarkdownPagesOpts::default()
494 ));
495}
496
497#[test]
498fn markdown_code_from_function_comment_is_trimmed() {
499 let mut config = PackageConfig::default();
500 config.name = EcoString::from("test_project_name");
501 let modules = vec![(
502 "app.gleam",
503 "
504/// Here's an example code snippet:
505/// ```
506/// wibble
507/// |> wobble
508/// ```
509///
510pub fn indentation_test() {
511 todo
512}
513",
514 )];
515 insta::assert_snapshot!(compile(config, modules));
516}
517
518#[test]
519fn markdown_code_from_module_comment_is_trimmed() {
520 let mut config = PackageConfig::default();
521 config.name = EcoString::from("test_project_name");
522 let modules = vec![(
523 "app.gleam",
524 "
525//// Here's an example code snippet:
526//// ```
527//// wibble
528//// |> wobble
529//// ```
530////
531",
532 )];
533 insta::assert_snapshot!(compile(config, modules));
534}
535
536#[test]
537fn doc_for_commented_definitions_is_not_included_in_next_constant() {
538 let mut config = PackageConfig::default();
539 config.name = EcoString::from("test_project_name");
540 let modules = vec![(
541 "app.gleam",
542 "
543/// Not included!
544// pub fn wibble() {}
545
546/// Included!
547pub const wobble = 1
548",
549 )];
550 assert!(!compile(config, modules).contains("Not included!"));
551}
552
553#[test]
554fn doc_for_commented_definitions_is_not_included_in_next_type() {
555 let mut config = PackageConfig::default();
556 config.name = EcoString::from("test_project_name");
557 let modules = vec![(
558 "app.gleam",
559 "
560/// Not included!
561// pub fn wibble() {}
562
563/// Included!
564pub type Wibble {
565 /// Wobble!
566 Wobble
567}
568",
569 )];
570 assert!(!compile(config, modules).contains("Not included!"));
571}
572
573#[test]
574fn doc_for_commented_definitions_is_not_included_in_next_function() {
575 let mut config = PackageConfig::default();
576 config.name = EcoString::from("test_project_name");
577 let modules = vec![(
578 "app.gleam",
579 "
580/// Not included!
581// pub fn wibble() {}
582
583/// Included!
584pub fn wobble(arg) {}
585",
586 )];
587 assert!(!compile(config, modules).contains("Not included!"));
588}
589
590#[test]
591fn doc_for_commented_definitions_is_not_included_in_next_type_alias() {
592 let mut config = PackageConfig::default();
593 config.name = EcoString::from("test_project_name");
594 let modules = vec![(
595 "app.gleam",
596 "
597/// Not included!
598// pub fn wibble() {}
599
600/// Included!
601pub type Wibble = Int
602",
603 )];
604 assert!(!compile(config, modules).contains("Not included!"));
605}
606
607#[test]
608fn source_link_for_github_repository() {
609 let mut config = PackageConfig::default();
610 config.name = EcoString::from("test_project_name");
611 config.repository = Some(Repository::GitHub {
612 user: "wibble".to_string(),
613 repo: "wobble".to_string(),
614 path: None,
615 tag_prefix: None,
616 });
617
618 let modules = vec![("app.gleam", "pub type Wibble = Int")];
619 assert!(
620 compile(config, modules)
621 .contains("https://github.com/wibble/wobble/blob/v0.1.0/src/app.gleam#L1")
622 );
623}
624
625#[test]
626fn source_link_for_github_repository_with_path_and_tag_prefix() {
627 let mut config = PackageConfig::default();
628 config.name = EcoString::from("test_project_name");
629 config.repository = Some(Repository::GitHub {
630 user: "wibble".to_string(),
631 repo: "wobble".to_string(),
632 path: Some("path/to/package".to_string()),
633 tag_prefix: Some("subdir-".into()),
634 });
635
636 let modules = vec![("app.gleam", "pub type Wibble = Int")];
637 assert!(compile(config, modules).contains(
638 "https://github.com/wibble/wobble/blob/subdir-v0.1.0/path/to/package/src/app.gleam#L1"
639 ));
640}
641
642#[test]
643fn canonical_link() {
644 let mut config = PackageConfig::default();
645 config.name = EcoString::from("test_project_name");
646 let modules = vec![
647 (
648 "app.gleam",
649 r#"
650/// Here is some documentation
651pub fn one() {
652 1
653}
654"#,
655 ),
656 (
657 "gleam/otp/actor.gleam",
658 r#"
659/// Here is some documentation
660pub fn one() {
661 1
662}
663"#,
664 ),
665 ];
666
667 let pages = vec![(
668 "LICENSE",
669 r#"
670# LICENSE
671 "#,
672 )];
673 insta::assert_snapshot!(compile_with_markdown_pages(
674 config,
675 modules,
676 pages,
677 CompileWithMarkdownPagesOpts::default()
678 ));
679}
680
681#[test]
682fn no_hex_publish() {
683 let mut config = PackageConfig::default();
684 config.name = EcoString::from("test_project_name");
685 let modules = vec![
686 (
687 "app.gleam",
688 r#"
689/// Here is some documentation
690pub fn one() {
691 1
692}
693"#,
694 ),
695 (
696 "gleam/otp/actor.gleam",
697 r#"
698/// Here is some documentation
699pub fn one() {
700 1
701}
702"#,
703 ),
704 ];
705
706 let pages = vec![(
707 "LICENSE",
708 r#"
709# LICENSE
710 "#,
711 )];
712 insta::assert_snapshot!(compile_with_markdown_pages(
713 config,
714 modules,
715 pages,
716 CompileWithMarkdownPagesOpts {
717 hex_publish: Some(DocContext::Build)
718 }
719 ));
720}
721
722fn create_sample_search_data() -> SearchData {
723 SearchData {
724 items: vec![
725 SearchItem {
726 type_: SearchItemType::Module,
727 parent_title: "gleam/option".to_string(),
728 title: "gleam/option".to_string(),
729 content: "".to_string(),
730 reference: "gleam/option.html".to_string(),
731 },
732 SearchItem {
733 type_: SearchItemType::Type,
734 parent_title: "gleam/option".to_string(),
735 title: "Option".to_string(),
736 content: "`Option` represents a value that may be present or not. `Some` means the value is present, `None` means the value is not.".to_string(),
737 reference: "gleam/option.html#Option".to_string(),
738 },
739 SearchItem {
740 type_: SearchItemType::Value,
741 parent_title: "gleam/option".to_string(),
742 title: "unwrap".to_string(),
743 content: "Extracts the value from an `Option`, returning a default value if there is none.".to_string(),
744 reference: "gleam/option.html#unwrap".to_string(),
745 },
746 SearchItem {
747 type_: SearchItemType::Value,
748 parent_title: "gleam/dynamic/decode".to_string(),
749 title: "bool".to_string(),
750 content: "A decoder that decodes `Bool` values.\n\n # Examples\n\n \n let result = decode.run(dynamic.from(True), decode.bool)\n assert result == Ok(True)\n \n".to_string(),
751 reference: "gleam/dynamic/decode.html#bool".to_string(),
752 },
753 ],
754 programming_language: SearchProgrammingLanguage::Gleam,
755 }
756}
757
758#[test]
759fn ensure_search_data_matches_exdocs_search_data_model_specification() {
760 let data = create_sample_search_data();
761 let json = serde_to_string(&data).unwrap();
762
763 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
764
765 // Ensure output of SearchData matches specification
766 assert!(parsed.is_object());
767 let obj = parsed.as_object().unwrap();
768 assert!(obj.contains_key("items"));
769 assert!(obj.contains_key("proglang"));
770
771 // Ensure output of SearchItem matches specification
772 let items = obj.get("items").unwrap().as_array().unwrap();
773 for item in items {
774 let item = item.as_object().unwrap();
775 assert!(item.contains_key("type"));
776 assert!(item.contains_key("parentTitle"));
777 assert!(item.contains_key("title"));
778 assert!(item.contains_key("doc"));
779 assert!(item.contains_key("ref"));
780 }
781}
782
783#[test]
784fn output_of_search_data_json() {
785 let data = create_sample_search_data();
786 let json = serde_to_string(&data).unwrap();
787 insta::assert_snapshot!(json);
788}
789
790const ONLY_LINKS: PrintOptions = PrintOptions {
791 print_highlighting: false,
792 print_html: true,
793};
794const NONE: PrintOptions = PrintOptions {
795 print_highlighting: false,
796 print_html: false,
797};
798
799#[test]
800fn highlight_function_definition() {
801 assert_documentation!(
802 "
803pub fn wibble(list: List(Int), generic: a, function: fn(a) -> b) -> #(a, b) { todo }
804"
805 );
806}
807
808#[test]
809fn highlight_constant_definition() {
810 assert_documentation!(
811 "
812pub const x = 22
813"
814 );
815}
816
817#[test]
818fn highlight_type_alias() {
819 assert_documentation!(
820 "
821pub type Option(a) = Result(a, Nil)
822"
823 );
824}
825
826#[test]
827fn highlight_custom_type() {
828 assert_documentation!(
829 "
830pub type Wibble(a, b) {
831 Wibble(a, i: Int)
832 Wobble(b: b, c: String)
833}
834"
835 );
836}
837
838#[test]
839fn highlight_opaque_custom_type() {
840 assert_documentation!(
841 "
842pub opaque type Wibble(a, b) {
843 Wibble(a, i: Int)
844 Wobble(b: b, c: String)
845}
846"
847 );
848}
849
850// https://github.com/gleam-lang/gleam/issues/2629
851#[test]
852fn print_type_variables_in_function_signatures() {
853 assert_documentation!(
854 "
855pub type Dict(key, value)
856
857pub fn insert(dict: Dict(key, value), key: key, value: value) -> Dict(key, value) {
858 dict
859}
860",
861 NONE
862 );
863}
864
865// https://github.com/gleam-lang/gleam/issues/828
866#[test]
867fn print_qualified_names_from_other_modules() {
868 assert_documentation!(
869 (
870 "gleam/option",
871 "
872pub type Option(t) {
873 Some(t)
874 None
875}
876"
877 ),
878 "
879import gleam/option.{type Option, Some, None}
880
881pub fn from_option(o: Option(t), e: e) -> Result(t, e) {
882 case o {
883 Some(t) -> Ok(t)
884 None -> Error(e)
885 }
886}
887",
888 NONE
889 );
890}
891
892// https://github.com/gleam-lang/gleam/issues/3461
893#[test]
894fn link_to_type_in_same_module() {
895 assert_documentation!(
896 "
897pub type Dict(a, b)
898
899pub fn new() -> Dict(a, b) { todo }
900",
901 ONLY_LINKS
902 );
903}
904
905// https://github.com/gleam-lang/gleam/issues/3461
906#[test]
907fn link_to_type_in_different_module() {
908 assert_documentation!(
909 ("gleam/dict", "pub type Dict(a, b)"),
910 "
911import gleam/dict
912
913pub fn make_dict() -> dict.Dict(a, b) { todo }
914",
915 ONLY_LINKS
916 );
917}
918
919#[test]
920fn link_to_type_in_different_module_from_nested_module() {
921 assert_documentation!(
922 ("gleam/dict", "pub type Dict(a, b)"),
923 "gleam/dynamic/decode",
924 "
925import gleam/dict
926
927pub fn decode_dict() -> dict.Dict(a, b) { todo }
928",
929 ONLY_LINKS
930 );
931}
932
933#[test]
934fn link_to_type_in_different_module_from_nested_module_with_shared_path() {
935 assert_documentation!(
936 ("gleam/dynamic", "pub type Dynamic"),
937 "gleam/dynamic/decode",
938 "
939import gleam/dynamic
940
941pub type Dynamic = dynamic.Dynamic
942",
943 ONLY_LINKS
944 );
945}
946
947// https://github.com/gleam-lang/gleam/issues/3461
948#[test]
949fn link_to_type_in_different_package() {
950 assert_documentation!(
951 ("gleam_stdlib", "gleam/dict", "pub type Dict(a, b)"),
952 "
953import gleam/dict
954
955pub fn make_dict() -> dict.Dict(a, b) { todo }
956",
957 ONLY_LINKS
958 );
959}
960
961#[test]
962fn no_link_to_type_in_git_dependency() {
963 assert_documentation!(
964 git: ("gleam_stdlib", "gleam/dict", "pub type Dict(a, b)"),
965 "
966import gleam/dict
967
968pub fn make_dict() -> dict.Dict(a, b) { todo }
969",
970 ONLY_LINKS
971 );
972}
973
974#[test]
975fn no_link_to_type_in_path_dependency() {
976 assert_documentation!(
977 path: ("gleam_stdlib", "gleam/dict", "pub type Dict(a, b)"),
978 "
979import gleam/dict
980
981pub fn make_dict() -> dict.Dict(a, b) { todo }
982",
983 ONLY_LINKS
984 );
985}
986
987#[test]
988fn no_links_to_prelude_types() {
989 assert_documentation!(
990 "
991pub fn int_to_string(i: Int) -> String { todo }
992",
993 ONLY_LINKS
994 );
995}
996
997#[test]
998fn generated_type_variables() {
999 assert_documentation!(
1000 "
1001pub fn wibble(_a, _b, _c, _d) {
1002 todo
1003}
1004",
1005 NONE
1006 );
1007}
1008
1009#[test]
1010fn generated_type_variables_mixed_with_existing_variables() {
1011 assert_documentation!(
1012 "
1013pub fn wibble(_a: b, _b: a, _c, _d) {
1014 todo
1015}
1016",
1017 NONE
1018 );
1019}
1020
1021#[test]
1022fn generated_type_variables_with_existing_variables_coming_afterwards() {
1023 assert_documentation!(
1024 "
1025pub fn wibble(_a, _b, _c: b, _d: a) {
1026 todo
1027}
1028",
1029 NONE
1030 );
1031}
1032
1033#[test]
1034fn generated_type_variables_do_not_take_into_account_other_definitions() {
1035 assert_documentation!(
1036 "
1037pub fn wibble(_a: a, _b: b, _c: c) -> d {
1038 todo
1039}
1040
1041pub fn identity(x) { x }
1042",
1043 NONE
1044 );
1045}
1046
1047#[test]
1048fn internal_type_reexport_in_same_module_as_parameter() {
1049 assert_documentation!(
1050 "
1051@internal
1052pub type Internal
1053
1054pub type External =
1055 List(Internal)
1056",
1057 ONLY_LINKS
1058 );
1059}
1060
1061#[test]
1062fn internal_type_reexport_in_same_module_as_parameter_colours() {
1063 assert_documentation!(
1064 "
1065@internal
1066pub type Internal
1067
1068pub type External =
1069 List(Internal)
1070",
1071 );
1072}
1073
1074#[test]
1075fn internal_type_reexport_in_same_module() {
1076 assert_documentation!(
1077 "
1078@internal
1079pub type Internal
1080
1081pub type External =
1082 Internal
1083",
1084 ONLY_LINKS
1085 );
1086}
1087
1088#[test]
1089fn internal_type_reexport_in_different_module() {
1090 assert_documentation!(
1091 ("other", "@internal pub type Internal"),
1092 "
1093import other
1094
1095pub type External =
1096 other.Internal
1097",
1098 ONLY_LINKS
1099 );
1100}
1101
1102#[test]
1103fn public_type_reexport_in_different_internal_module() {
1104 assert_documentation!(
1105 ("thepackage/internal/other", "pub type Internal"),
1106 "
1107import thepackage/internal/other
1108
1109pub type External =
1110 other.Internal
1111",
1112 ONLY_LINKS
1113 );
1114}
1115
1116#[test]
1117fn use_reexport_from_other_package() {
1118 assert_documentation!(
1119 ("some_package", "some_package/internal", "pub type Internal"),
1120 (
1121 "some_package",
1122 "some_package/api",
1123 "
1124import some_package/internal
1125pub type External = internal.Internal
1126"
1127 ),
1128 "
1129import some_package/api
1130
1131pub fn do_thing(value: api.External) {
1132 value
1133}
1134",
1135 ONLY_LINKS
1136 );
1137}
1138
1139#[test]
1140fn function_uses_reexport_of_internal_type() {
1141 assert_documentation!(
1142 ("thepackage/internal", "pub type Internal"),
1143 "
1144import thepackage/internal
1145
1146pub type External = internal.Internal
1147
1148pub fn do_thing(value: internal.Internal) -> External {
1149 value
1150}
1151",
1152 ONLY_LINKS
1153 );
1154}
1155
1156#[test]
1157fn function_uses_reexport_of_internal_type_in_other_module() {
1158 assert_documentation!(
1159 ("thepackage/internal", "pub type Internal"),
1160 (
1161 "thepackage/something",
1162 "
1163import thepackage/internal
1164
1165pub type External = internal.Internal
1166"
1167 ),
1168 "
1169import thepackage/something
1170
1171pub fn do_thing(value: something.External) {
1172 value
1173}
1174",
1175 ONLY_LINKS
1176 );
1177}
1178
1179#[test]
1180fn constructor_with_long_types_and_many_fields() {
1181 assert_documentation!(
1182 ("option", "pub type Option(a)"),
1183 "
1184import option
1185
1186pub type Uri {
1187 Uri(
1188 scheme: option.Option(String),
1189 userinfo: option.Option(String),
1190 host: option.Option(String),
1191 port: option.Option(Int),
1192 path: String,
1193 query: option.Option(String),
1194 fragment: option.Option(String)
1195 )
1196}
1197",
1198 NONE
1199 );
1200}
1201
1202#[test]
1203fn constructor_with_long_types_and_many_fields_that_need_splitting() {
1204 assert_documentation!(
1205 ("option", "pub type Option(a)"),
1206 "
1207import option
1208
1209pub type TypeWithAVeryLoooooooooooooooooooongName
1210
1211pub type Wibble {
1212 Wibble(
1213 wibble: #(TypeWithAVeryLoooooooooooooooooooongName, TypeWithAVeryLoooooooooooooooooooongName),
1214 wobble: option.Option(String),
1215 )
1216}
1217",
1218 NONE
1219 );
1220}
1221
1222#[test]
1223fn gitea_repository_url_has_no_double_slash() {
1224 let repo = Repository::Forgejo {
1225 host: "https://code.example.org/".parse::<Uri>().unwrap(),
1226 user: "person".into(),
1227 repo: "forgejo_bug".into(),
1228 path: None,
1229 tag_prefix: None,
1230 };
1231
1232 assert_eq!(repo.url(), "https://code.example.org/person/forgejo_bug");
1233}
1234
1235#[test]
1236fn long_function_with_no_arguments_parentheses_are_not_split() {
1237 assert_documentation!(
1238 "
1239pub fn aaaaaaaaaaaaaaaaaaaaaaaaaaaa() -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {
1240 todo
1241}
1242",
1243 NONE
1244 );
1245}
1246
1247#[test]
1248fn forgejo_single_line_definition() {
1249 let mut config = PackageConfig::default();
1250 let repo = Repository::Forgejo {
1251 host: "https://code.example.org/".parse::<Uri>().unwrap(),
1252 user: "wibble".into(),
1253 repo: "wobble".into(),
1254 path: None,
1255 tag_prefix: None,
1256 };
1257
1258 config.name = EcoString::from("test_project_name");
1259 config.repository = Some(repo);
1260
1261 let modules = vec![("app.gleam", "pub type Wibble = Int")];
1262 let html = compile(config, modules);
1263
1264 assert!(
1265 html.contains("https://code.example.org/wibble/wobble/src/tag/v0.1.0/src/app.gleam#L1")
1266 );
1267}
1268
1269#[test]
1270fn forgejo_multiple_line_definition() {
1271 let mut config = PackageConfig::default();
1272 let repo = Repository::Forgejo {
1273 host: "https://code.example.org/".parse::<Uri>().unwrap(),
1274 user: "wibble".into(),
1275 repo: "wobble".into(),
1276 path: None,
1277 tag_prefix: None,
1278 };
1279
1280 config.name = EcoString::from("test_project_name");
1281 config.repository = Some(repo);
1282
1283 let modules = vec![("app.gleam", "pub type Wibble \n\n= Int")];
1284 let html = compile(config, modules);
1285
1286 assert!(
1287 html.contains("https://code.example.org/wibble/wobble/src/tag/v0.1.0/src/app.gleam#L1-L3")
1288 );
1289}
1290
1291fn generate_search_data(module_name: &str, module_src: &str) -> EcoString {
1292 let module = type_::tests::compile_module(module_name, module_src, None, Vec::new())
1293 .expect("Module should compile successfully");
1294
1295 let mut config = PackageConfig::default();
1296 config.name = "thepackage".into();
1297 let paths = ProjectPaths::new("/".into());
1298 let build_module = build::Module {
1299 name: "main".into(),
1300 code: module_src.into(),
1301 mtime: SystemTime::now(),
1302 input_path: "/".into(),
1303 origin: Origin::Src,
1304 ast: module,
1305 extra: ModuleExtra::new(),
1306 dependencies: vec![],
1307 };
1308
1309 let source_links = SourceLinker::new(&paths, &config, &build_module);
1310
1311 let module = &build_module.ast;
1312
1313 let dependencies = HashMap::new();
1314 let mut printer = Printer::new(
1315 module.type_info.package.clone(),
1316 module.name.clone(),
1317 &module.names,
1318 &dependencies,
1319 );
1320
1321 let mut search_items = Vec::new();
1322
1323 let types = printer.type_definitions(&source_links, &module.definitions);
1324 let values = printer.value_definitions(&source_links, &module.definitions);
1325
1326 search_items.push(search_item_for_module(&build_module));
1327
1328 for type_ in types {
1329 search_items.push(search_item_for_type(module_name, &type_));
1330 }
1331 for value in values {
1332 search_items.push(search_item_for_value(module_name, &value));
1333 }
1334
1335 let mut output = EcoString::new();
1336
1337 output.push_str("------ SOURCE CODE\n");
1338 output.push_str(module_src);
1339 output.push_str("\n------------------------------------\n\n");
1340
1341 for item in search_items {
1342 let SearchItem {
1343 type_,
1344 parent_title,
1345 title,
1346 content,
1347 reference,
1348 } = item;
1349
1350 let type_ = match type_ {
1351 SearchItemType::Value => "Value",
1352 SearchItemType::Module => "Module",
1353 SearchItemType::Page => "Page",
1354 SearchItemType::Type => "Type",
1355 };
1356
1357 output.push_str(&eco_format!(
1358 "TITLE: {title}
1359PARENT TITLE: {parent_title}
1360TYPE: {type_}
1361REFERENCE: {reference}
1362CONTENT:
1363{content}
1364------------------------------------
1365"
1366 ));
1367 }
1368
1369 output
1370}
1371
1372#[test]
1373fn search_item_for_custom_type() {
1374 let output = generate_search_data(
1375 "module",
1376 "
1377/// # The `Option` type
1378/// Represents an optional value, either `Some` or `None`.
1379/// If it is None, the value is absent
1380/// [Read more](https://example.com)
1381pub type Option(inner) {
1382 /// Here is some
1383 /// documentation for the `Some` constructor
1384 Some(
1385 /// And even documentation on a **field**!
1386 value: inner
1387 )
1388 None
1389}
1390",
1391 );
1392 insta::assert_snapshot!(output);
1393}
1394
1395#[test]
1396fn search_item_for_type_alias() {
1397 let output = generate_search_data(
1398 "module",
1399 "
1400/// This is a type alias to a list
1401/// of integer values.
1402/// ## Examples
1403/// ```
1404/// // Examples
1405/// ```
1406pub type IntList = List(Int)
1407",
1408 );
1409 insta::assert_snapshot!(output);
1410}
1411
1412#[test]
1413fn search_item_for_function() {
1414 let output = generate_search_data(
1415 "module",
1416 "
1417/// Pi is the ration between a circle's **radius** and its
1418/// *circumference*. Pretty cool!
1419pub const pi = 3.14
1420",
1421 );
1422 insta::assert_snapshot!(output);
1423}
1424
1425#[test]
1426fn search_item_for_constant() {
1427 let output = generate_search_data(
1428 "module",
1429 "
1430/// Reverses a `List`, and returns the list in reverse.
1431/// ```
1432/// reverse([1, 2, 3])
1433/// // [3, 2, 1]
1434/// ```
1435pub fn reverse(list: List(a), out: List(a)) -> List(a) {
1436 case list {
1437 [] -> out
1438 [first, ..rest] -> reverse(rest, [first, ..out])
1439 }
1440}
1441",
1442 );
1443 insta::assert_snapshot!(output);
1444}