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