Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / docs / tests.rs
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}