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