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 / printer.rs
24 kB 685 lines
1use std::{ 2 collections::{HashMap, HashSet}, 3 ops::Deref, 4}; 5 6use ecow::{EcoString, eco_format}; 7use itertools::Itertools; 8 9use crate::{ 10 ast::{ 11 ArgNames, CustomType, Definition, Function, ModuleConstant, Publicity, 12 RecordConstructorArg, SrcSpan, TypeAlias, TypedArg, TypedDefinition, 13 TypedRecordConstructor, 14 }, 15 docvec, 16 pretty::{Document, Documentable, break_, join, line, nil, zero_width_string}, 17 type_::{ 18 Deprecation, PRELUDE_MODULE_NAME, PRELUDE_PACKAGE_NAME, Type, TypeVar, printer::Names, 19 }, 20}; 21 22use super::{ 23 Dependency, DependencyKind, DocsValues, TypeConstructor, TypeConstructorArg, TypeDefinition, 24 markdown_documentation, source_links::SourceLinker, text_documentation, 25}; 26 27#[derive(Clone, Copy)] 28pub struct PrintOptions { 29 pub print_highlighting: bool, 30 pub print_html: bool, 31} 32 33impl PrintOptions { 34 pub fn all() -> Self { 35 Self { 36 print_highlighting: true, 37 print_html: true, 38 } 39 } 40} 41 42pub struct Printer<'a> { 43 options: PrintOptions, 44 names: &'a Names, 45 46 package: EcoString, 47 module: EcoString, 48 49 /// Type variables which don't have annotated names and we have generated 50 /// names for. 51 printed_type_variables: HashMap<u64, EcoString>, 52 /// Names of type variables that we have generated or printed in a given 53 /// definition. This ensures that we have no duplicate generated names. 54 printed_type_variable_names: HashSet<EcoString>, 55 /// An incrementing number used to generate the next type variable name. 56 /// `0` becomes `a`, `1` becomes `b`, etc. 57 next_type_variable_id: u64, 58 59 dependencies: &'a HashMap<EcoString, Dependency>, 60} 61 62impl Printer<'_> { 63 pub fn new<'a>( 64 package: EcoString, 65 module: EcoString, 66 names: &'a Names, 67 dependencies: &'a HashMap<EcoString, Dependency>, 68 ) -> Printer<'a> { 69 Printer { 70 options: PrintOptions::all(), 71 names, 72 package, 73 module, 74 printed_type_variables: HashMap::new(), 75 printed_type_variable_names: HashSet::new(), 76 next_type_variable_id: 0, 77 dependencies, 78 } 79 } 80 81 // This is currently only used in the tests, though it might be useful in 82 // application code in future. If it is needed, simply remove this attribute. 83 #[cfg(test)] 84 pub fn set_options(&mut self, options: PrintOptions) { 85 self.options = options; 86 } 87 88 pub fn type_definition<'a>( 89 &mut self, 90 source_links: &SourceLinker, 91 statement: &'a TypedDefinition, 92 ) -> Option<TypeDefinition<'a>> { 93 match statement { 94 Definition::CustomType(CustomType { 95 publicity: Publicity::Public, 96 location, 97 name, 98 constructors, 99 documentation, 100 deprecation, 101 opaque, 102 parameters, 103 .. 104 }) => Some(TypeDefinition { 105 name, 106 definition: print(self.custom_type(name, parameters, constructors, *opaque)), 107 documentation: markdown_documentation(documentation), 108 text_documentation: text_documentation(documentation), 109 deprecation_message: match deprecation { 110 Deprecation::NotDeprecated => "".to_string(), 111 Deprecation::Deprecated { message } => message.to_string(), 112 }, 113 constructors: if *opaque { 114 Vec::new() 115 } else { 116 constructors 117 .iter() 118 .map(|constructor| TypeConstructor { 119 definition: print(self.record_constructor(constructor)), 120 documentation: markdown_documentation(&constructor.documentation), 121 text_documentation: text_documentation(&constructor.documentation), 122 arguments: constructor 123 .arguments 124 .iter() 125 .filter_map(|arg| arg.label.as_ref().map(|(_, label)| (arg, label))) 126 .map(|(argument, label)| TypeConstructorArg { 127 name: label.trim_end().to_string(), 128 doc: markdown_documentation(&argument.doc), 129 }) 130 .filter(|arg| !arg.doc.is_empty()) 131 .collect(), 132 }) 133 .collect() 134 }, 135 source_url: source_links.url(*location), 136 opaque: *opaque, 137 }), 138 139 Definition::TypeAlias(TypeAlias { 140 publicity: Publicity::Public, 141 location, 142 alias: name, 143 parameters, 144 type_, 145 documentation, 146 deprecation, 147 .. 148 }) => Some(TypeDefinition { 149 name, 150 definition: print(self.type_alias(name, type_, parameters).group()), 151 documentation: markdown_documentation(documentation), 152 text_documentation: text_documentation(documentation), 153 constructors: vec![], 154 source_url: source_links.url(*location), 155 deprecation_message: match deprecation { 156 Deprecation::NotDeprecated => "".to_string(), 157 Deprecation::Deprecated { message } => message.to_string(), 158 }, 159 opaque: false, 160 }), 161 162 Definition::TypeAlias(_) 163 | Definition::CustomType(_) 164 | Definition::Function(_) 165 | Definition::Import(_) 166 | Definition::ModuleConstant(_) => None, 167 } 168 } 169 170 pub fn value<'a>( 171 &mut self, 172 source_links: &SourceLinker, 173 statement: &'a TypedDefinition, 174 ) -> Option<DocsValues<'a>> { 175 // Ensure that any type variables we printed in previous definitions don't 176 // affect our printing of this definition. Two type variables in different 177 // definitions can have the same name without clashing. 178 self.printed_type_variable_names.clear(); 179 self.next_type_variable_id = 0; 180 181 match statement { 182 Definition::Function(Function { 183 publicity: Publicity::Public, 184 name: Some((_, name)), 185 documentation: doc, 186 location, 187 deprecation, 188 arguments, 189 return_type, 190 .. 191 }) => Some(DocsValues { 192 name, 193 definition: print(self.function_signature(name, arguments, return_type)), 194 documentation: markdown_documentation(doc), 195 text_documentation: text_documentation(doc), 196 source_url: source_links.url(*location), 197 deprecation_message: match deprecation { 198 Deprecation::NotDeprecated => "".to_string(), 199 Deprecation::Deprecated { message } => message.to_string(), 200 }, 201 }), 202 203 Definition::ModuleConstant(ModuleConstant { 204 publicity: Publicity::Public, 205 documentation, 206 location, 207 name, 208 type_, 209 deprecation, 210 .. 211 }) => Some(DocsValues { 212 name, 213 definition: print(self.constant(name, type_)), 214 documentation: markdown_documentation(documentation), 215 text_documentation: text_documentation(documentation), 216 source_url: source_links.url(*location), 217 deprecation_message: match deprecation { 218 Deprecation::NotDeprecated => "".to_string(), 219 Deprecation::Deprecated { message } => message.to_string(), 220 }, 221 }), 222 223 Definition::TypeAlias(_) 224 | Definition::CustomType(_) 225 | Definition::Function(_) 226 | Definition::Import(_) 227 | Definition::ModuleConstant(_) => None, 228 } 229 } 230 231 fn custom_type<'a>( 232 &mut self, 233 name: &'a str, 234 parameters: &'a [(SrcSpan, EcoString)], 235 constructors: &'a [TypedRecordConstructor], 236 opaque: bool, 237 ) -> Document<'a> { 238 let arguments = if parameters.is_empty() { 239 nil() 240 } else { 241 Self::wrap_arguments( 242 parameters 243 .iter() 244 .map(|(_, parameter)| self.variable(parameter)), 245 ) 246 }; 247 248 let keywords = if opaque { 249 "pub opaque type " 250 } else { 251 "pub type " 252 }; 253 254 let type_head = docvec![self.keyword(keywords), self.title(name), arguments]; 255 256 if constructors.is_empty() || opaque { 257 return type_head; 258 } 259 260 let constructors = constructors 261 .iter() 262 .map(|constructor| { 263 line() 264 .append(self.record_constructor(constructor)) 265 .nest(INDENT) 266 }) 267 .collect_vec(); 268 269 docvec![type_head, " {", constructors, line(), "}"] 270 } 271 272 pub fn record_constructor<'a>( 273 &mut self, 274 constructor: &'a TypedRecordConstructor, 275 ) -> Document<'a> { 276 if constructor.arguments.is_empty() { 277 return self.title(&constructor.name); 278 } 279 280 let arguments = constructor.arguments.iter().map( 281 |RecordConstructorArg { label, type_, .. }| match label { 282 Some((_, label)) => self.variable(label).append(": ").append(self.type_(type_)), 283 None => self.type_(type_), 284 }, 285 ); 286 287 let arguments = Self::wrap_arguments(arguments); 288 289 docvec![self.title(&constructor.name), arguments].group() 290 } 291 292 fn type_alias<'a>( 293 &mut self, 294 name: &'a str, 295 type_: &Type, 296 parameters: &[(SrcSpan, EcoString)], 297 ) -> Document<'a> { 298 let parameters = if parameters.is_empty() { 299 nil() 300 } else { 301 let arguments = parameters 302 .iter() 303 .map(|(_, parameter)| self.variable(parameter)); 304 Self::wrap_arguments(arguments) 305 }; 306 307 docvec![ 308 self.keyword("pub type "), 309 self.title(name), 310 parameters, 311 " =", 312 line().append(self.type_(type_)).nest(INDENT) 313 ] 314 } 315 316 fn constant<'a>(&mut self, name: &'a str, type_: &Type) -> Document<'a> { 317 self.register_local_type_variable_names(type_); 318 319 docvec![ 320 self.keyword("pub const "), 321 self.title(name), 322 ": ", 323 self.type_(type_) 324 ] 325 } 326 327 fn function_signature<'a>( 328 &mut self, 329 name: &'a str, 330 arguments: &'a [TypedArg], 331 return_type: &Type, 332 ) -> Document<'a> { 333 for argument in arguments { 334 self.register_local_type_variable_names(&argument.type_); 335 } 336 self.register_local_type_variable_names(return_type); 337 338 let arguments = if arguments.is_empty() { 339 "()".to_doc() 340 } else { 341 Self::wrap_arguments(arguments.iter().map(|argument| { 342 let name = self.variable(self.argument_name(argument)); 343 docvec![name, ": ", self.type_(&argument.type_)].group() 344 })) 345 }; 346 347 docvec![ 348 self.keyword("pub fn "), 349 self.title(name), 350 arguments, 351 " -> ", 352 self.type_(return_type) 353 ] 354 .group() 355 } 356 357 fn argument_name<'a>(&self, arg: &'a TypedArg) -> Document<'a> { 358 match &arg.names { 359 ArgNames::Named { name, .. } => name.to_doc(), 360 ArgNames::NamedLabelled { label, name, .. } => docvec![label, " ", name], 361 // We remove the underscore from discarded function arguments since we don't want to 362 // expose this kind of detail: https://github.com/gleam-lang/gleam/issues/2561 363 ArgNames::Discard { name, .. } => match name.strip_prefix('_').unwrap_or(name) { 364 "" => "arg".to_doc(), 365 name => name.to_doc(), 366 }, 367 ArgNames::LabelledDiscard { label, name, .. } => { 368 docvec![label, " ", name.strip_prefix('_').unwrap_or(name).to_doc()] 369 } 370 } 371 } 372 373 fn wrap_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> { 374 break_("(", "(") 375 .append(join(arguments, break_(",", ", "))) 376 .nest_if_broken(INDENT) 377 .append(break_(",", "")) 378 .append(")") 379 } 380 381 fn type_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> { 382 break_("", "") 383 .append(join(arguments, break_(",", ", "))) 384 .nest_if_broken(INDENT) 385 .append(break_(",", "")) 386 .group() 387 .surround("(", ")") 388 } 389 390 fn type_(&mut self, type_: &Type) -> Document<'static> { 391 match type_ { 392 Type::Named { 393 package, 394 module, 395 name, 396 args, 397 publicity, 398 .. 399 } => { 400 let name = self.named_type_name(publicity, package, module, name); 401 if args.is_empty() { 402 name 403 } else { 404 name.append(Self::type_arguments( 405 args.iter().map(|argument| self.type_(argument)), 406 )) 407 } 408 } 409 Type::Fn { args, return_ } => docvec![ 410 self.keyword("fn"), 411 Self::type_arguments(args.iter().map(|argument| self.type_(argument))), 412 " -> ", 413 self.type_(return_) 414 ], 415 Type::Tuple { elements } => docvec![ 416 "#", 417 Self::type_arguments(elements.iter().map(|element| self.type_(element))), 418 ], 419 Type::Var { type_ } => match type_.as_ref().borrow().deref() { 420 TypeVar::Link { type_ } => self.type_(type_), 421 422 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 423 let name = self.type_variable(*id); 424 self.variable(name) 425 } 426 }, 427 } 428 } 429 430 fn type_variable(&mut self, id: u64) -> EcoString { 431 if let Some(name) = self.names.get_type_variable(id) { 432 return name.clone(); 433 } 434 435 if let Some(name) = self.printed_type_variables.get(&id) { 436 return name.clone(); 437 } 438 439 loop { 440 let name = self.next_letter(); 441 if !self.printed_type_variable_names.contains(&name) { 442 _ = self.printed_type_variable_names.insert(name.clone()); 443 _ = self.printed_type_variables.insert(id, name.clone()); 444 return name; 445 } 446 } 447 } 448 449 // Copied from the `next_letter` method of the `type_::printer`. 450 fn next_letter(&mut self) -> EcoString { 451 let alphabet_length = 26; 452 let char_offset = b'a'; 453 let mut chars = vec![]; 454 let mut n; 455 let mut rest = self.next_type_variable_id; 456 457 loop { 458 n = rest % alphabet_length; 459 rest = rest / alphabet_length; 460 chars.push((n as u8 + char_offset) as char); 461 462 if rest == 0 { 463 break; 464 } 465 rest -= 1 466 } 467 468 self.next_type_variable_id += 1; 469 chars.into_iter().rev().collect() 470 } 471 472 fn named_type_name( 473 &self, 474 publicity: &Publicity, 475 package: &str, 476 module: &str, 477 name: &EcoString, 478 ) -> Document<'static> { 479 // There's no documentation page for the prelude 480 if package == PRELUDE_PACKAGE_NAME && module == PRELUDE_MODULE_NAME { 481 return self.title(name); 482 } 483 484 // Internal types don't get linked 485 if !publicity.is_public() { 486 return docvec![self.comment("@internal ".to_doc()), self.title(name)]; 487 } 488 489 // Linking to a type within the same page 490 if package == self.package && module == self.module { 491 return self.link(eco_format!("#{name}"), self.title(name), None); 492 } 493 494 // Linking to a module within the package 495 if package == self.package { 496 // If we are linking to the current package, we might be viewing the 497 // documentation locally and so we need to generate a relative link. 498 499 let mut module_path = module.split('/').peekable(); 500 let mut current_module = self.module.split('/'); 501 502 // The documentation page for the final segment of the module is just 503 // an html file by itself, so it doesn't form part of the path and doesn't 504 // need to be backtracked using `..`. 505 let module_name = module_path.next_back().unwrap_or(module); 506 _ = current_module.next_back(); 507 508 // The two modules might have some sharer part of the path, which we 509 // don't need to traverse back through. However, if the two modules are 510 // something like `gleam/a/wibble/wobble` and `gleam/b/wibble/wobble`, 511 // the `wibble` folders are two different folders despite being at the 512 // same position with the same name. 513 let mut encountered_different_path = false; 514 let mut path = Vec::new(); 515 516 // Calculate how far backwards in the directory tree we need to walk 517 for segment in current_module { 518 // If this is still part of the shared path, we can just skip it: 519 // no need to go back and forth through the same directory in the 520 // path! 521 if !encountered_different_path && module_path.peek() == Some(&segment) { 522 _ = module_path.next(); 523 } else { 524 encountered_different_path = true; 525 path.push(".."); 526 } 527 } 528 529 // Once we have walked backwards, we walk forwards again to the correct 530 // page. 531 path.extend(module_path); 532 path.push(module_name); 533 534 let qualified_name = docvec![ 535 self.variable(EcoString::from(module_name)), 536 ".", 537 self.title(name) 538 ]; 539 540 let title = eco_format!("{module}.{{type {name}}}"); 541 542 return self.link( 543 eco_format!("{path}.html#{name}", path = path.join("/")), 544 qualified_name, 545 Some(title), 546 ); 547 } 548 549 let module_name = module.split('/').next_back().unwrap_or(module); 550 let qualified_name = docvec![ 551 self.variable(EcoString::from(module_name)), 552 ".", 553 self.title(name) 554 ]; 555 let title = eco_format!("{module}.{{type {name}}}"); 556 557 // We can't reliably link to documentation if the type is from a path 558 // or git dependency 559 match self.dependencies.get(package) { 560 Some(Dependency { 561 kind: DependencyKind::Hex, 562 version, 563 }) => self.link( 564 eco_format!("https://hexdocs.pm/{package}/{version}/{module}.html#{name}"), 565 qualified_name, 566 Some(title), 567 ), 568 Some(_) | None => self.span_with_title(qualified_name, title), 569 } 570 } 571 572 /// Walk a type and register all the type variable names which occur within 573 /// it. This is to ensure that when generating type variable names for 574 /// unannotated arguments, we don't print any that clash with existing names. 575 /// 576 /// We preregister all names before actually printing anything, because we 577 /// could run into code like this: 578 /// 579 /// ```gleam 580 /// pub fn wibble(_, _: a) -> b {} 581 /// ``` 582 /// 583 /// If we did not preregister the type variables in this case, we would end 584 /// up printing `fn wibble(_: a, _: a) -> b` which is not correct. 585 /// 586 fn register_local_type_variable_names(&mut self, type_: &Type) { 587 match type_ { 588 Type::Named { args, .. } => { 589 for arg in args { 590 self.register_local_type_variable_names(arg); 591 } 592 } 593 Type::Fn { args, return_ } => { 594 for arg in args { 595 self.register_local_type_variable_names(arg); 596 } 597 self.register_local_type_variable_names(return_); 598 } 599 Type::Var { type_ } => match type_.borrow().deref() { 600 TypeVar::Link { type_ } => self.register_local_type_variable_names(type_), 601 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 602 if let Some(name) = self.names.get_type_variable(*id) { 603 _ = self.printed_type_variable_names.insert(name.clone()); 604 } 605 } 606 }, 607 Type::Tuple { elements } => { 608 for element in elements { 609 self.register_local_type_variable_names(element); 610 } 611 } 612 } 613 } 614 615 fn keyword<'a>(&self, keyword: impl Documentable<'a>) -> Document<'a> { 616 self.colour_span(keyword, "keyword") 617 } 618 619 fn comment<'a>(&self, name: impl Documentable<'a>) -> Document<'a> { 620 self.colour_span(name, "comment") 621 } 622 623 fn title<'a>(&self, name: impl Documentable<'a>) -> Document<'a> { 624 self.colour_span(name, "title") 625 } 626 627 fn variable<'a>(&self, name: impl Documentable<'a>) -> Document<'a> { 628 self.colour_span(name, "variable") 629 } 630 631 fn colour_span<'a>( 632 &self, 633 name: impl Documentable<'a>, 634 colour_class: &'static str, 635 ) -> Document<'a> { 636 if !self.options.print_highlighting { 637 return name.to_doc(); 638 } 639 640 name.to_doc().surround( 641 zero_width_string(eco_format!(r#"<span class="hljs-{colour_class}">"#)), 642 zero_width_string("</span>".into()), 643 ) 644 } 645 646 fn link<'a>( 647 &self, 648 href: EcoString, 649 name: impl Documentable<'a>, 650 title: Option<EcoString>, 651 ) -> Document<'a> { 652 if !self.options.print_html { 653 return name.to_doc(); 654 } 655 656 let opening_tag = if let Some(title) = title { 657 eco_format!(r#"<a href="{href}" title="{title}">"#) 658 } else { 659 eco_format!(r#"<a href="{href}">"#) 660 }; 661 662 name.to_doc().surround( 663 zero_width_string(opening_tag), 664 zero_width_string("</a>".into()), 665 ) 666 } 667 668 fn span_with_title<'a>(&self, name: impl Documentable<'a>, title: EcoString) -> Document<'a> { 669 if !self.options.print_html { 670 return name.to_doc(); 671 } 672 673 name.to_doc().surround( 674 zero_width_string(eco_format!(r#"<span title="{title}">"#)), 675 zero_width_string("</span>".into()), 676 ) 677 } 678} 679 680const MAX_COLUMNS: isize = 65; 681const INDENT: isize = 2; 682 683fn print(doc: Document<'_>) -> String { 684 doc.to_pretty_string(MAX_COLUMNS) 685}