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