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