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