microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
280b0da953f54bd2ea9e48c78f03518ef4e75bae

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

compiler/qsc_doc_gen/src/generate_docs.rs

715lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#[cfg(test)]
5mod tests;
6
7use crate::display::{increase_header_level, parse_doc_for_summary};
8use crate::display::{CodeDisplay, Lookup};
9use crate::table_of_contents::table_of_contents;
10use qsc_ast::ast;
11use qsc_data_structures::language_features::LanguageFeatures;
12use qsc_data_structures::target::TargetCapabilityFlags;
13use qsc_frontend::compile::{self, compile, Dependencies, PackageStore, SourceMap};
14use qsc_frontend::resolve;
15use qsc_hir::hir::{CallableKind, Item, ItemKind, Package, PackageId, Visibility};
16use qsc_hir::{hir, ty};
17use rustc_hash::FxHashMap;
18use std::fmt::{Display, Formatter, Result};
19use std::rc::Rc;
20use std::sync::Arc;
21
22// Name, Metadata, Content
23type Files = Vec<(Rc<str>, Rc<str>, Rc<str>)>;
24type FilesWithMetadata = Vec<(Rc<str>, Rc<Metadata>, Rc<str>)>;
25
26// Namespace -> metadata for items
27type ToC = FxHashMap<Rc<str>, Vec<Rc<Metadata>>>;
28
29struct Metadata {
30 uid: String,
31 title: String,
32 topic: String,
33 kind: MetadataKind,
34 package: PackageKind,
35 namespace: Rc<str>,
36 name: Rc<str>,
37 summary: String,
38 signature: String,
39}
40
41impl Metadata {
42 fn fully_qualified_name(&self) -> String {
43 let mut buf = if let PackageKind::AliasedPackage(ref package_alias) = self.package {
44 vec![format!("{package_alias}")]
45 } else {
46 vec![]
47 };
48
49 buf.push(self.namespace.to_string());
50 buf.push(self.name.to_string());
51 buf.join(".")
52 }
53
54 fn display_for_toc(&self) -> String {
55 format!(
56 "---
57uid: {}
58title: {}
59description: {}
60author: {{AUTHOR}}
61ms.author: {{MS_AUTHOR}}
62ms.date: {{TIMESTAMP}}
63ms.topic: {}
64---",
65 self.uid, self.title, self.summary, self.topic,
66 )
67 }
68
69 fn display_for_item(&self) -> String {
70 let kind = match &self.kind {
71 MetadataKind::Function => "function",
72 MetadataKind::Operation => "operation",
73 MetadataKind::Udt => "udt",
74 MetadataKind::Export => "export",
75 MetadataKind::TableOfContents => "table of contents",
76 };
77 format!(
78 "---
79uid: {}
80title: {}
81description: \"Q# {}: {}\"
82ms.date: {{TIMESTAMP}}
83ms.topic: {}
84qsharp.kind: {}
85qsharp.package: {}
86qsharp.namespace: {}
87qsharp.name: {}
88qsharp.summary: \"{}\"
89---",
90 self.uid,
91 self.title,
92 self.title,
93 self.summary,
94 self.topic,
95 kind,
96 self.package,
97 self.namespace,
98 self.name,
99 self.summary
100 )
101 }
102}
103
104impl Display for Metadata {
105 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
106 if self.kind == MetadataKind::TableOfContents {
107 write!(f, "{}", self.display_for_toc())
108 } else {
109 write!(f, "{}", self.display_for_item())
110 }
111 }
112}
113
114#[derive(PartialOrd, Ord, Eq, PartialEq, Clone)]
115enum MetadataKind {
116 Function,
117 Operation,
118 Udt,
119 Export,
120 TableOfContents,
121}
122
123impl Display for MetadataKind {
124 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
125 let s = match &self {
126 MetadataKind::Function => "function",
127 MetadataKind::Operation => "operation",
128 MetadataKind::Udt => "user defined type",
129 MetadataKind::Export => "exported item",
130 MetadataKind::TableOfContents => "table of contents",
131 };
132 write!(f, "{s}")
133 }
134}
135
136#[derive(PartialOrd, Ord, Eq, PartialEq, Clone)]
137enum PackageKind {
138 UserCode,
139 AliasedPackage(String),
140 StandardLibrary,
141 Core,
142}
143
144impl Display for PackageKind {
145 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
146 let s = match &self {
147 PackageKind::UserCode => "__Main__",
148 PackageKind::AliasedPackage(alias) => alias,
149 PackageKind::StandardLibrary => "__Std__",
150 PackageKind::Core => "__Core__",
151 };
152 write!(f, "{s}")
153 }
154}
155
156/// Represents an immutable compilation state.
157#[derive(Debug)]
158struct Compilation {
159 /// Package store, containing the current package and all its dependencies.
160 package_store: PackageStore,
161 /// Current package id when provided.
162 current_package_id: Option<PackageId>,
163 /// Aliases for packages.
164 dependencies: FxHashMap<PackageId, Arc<str>>,
165}
166
167impl Compilation {
168 /// Creates a new `Compilation` by compiling standard library
169 /// and additional sources.
170 pub(crate) fn new(
171 additional_program: Option<(PackageStore, &Dependencies, SourceMap)>,
172 capabilities: Option<TargetCapabilityFlags>,
173 language_features: Option<LanguageFeatures>,
174 ) -> Self {
175 let actual_capabilities = capabilities.unwrap_or_default();
176 let actual_language_features = language_features.unwrap_or_default();
177
178 let mut current_package_id: Option<PackageId> = None;
179 let mut package_aliases: FxHashMap<PackageId, Arc<str>> = FxHashMap::default();
180
181 let package_store =
182 if let Some((mut package_store, dependencies, sources)) = additional_program {
183 let unit = compile(
184 &package_store,
185 dependencies,
186 sources,
187 actual_capabilities,
188 actual_language_features,
189 );
190 // We ignore errors here (unit.errors vector) and use whatever
191 // documentation we can produce. In future we may consider
192 // displaying the fact of error presence on documentation page.
193
194 for (package_id, package_alias) in dependencies {
195 if let Some(package_alias) = package_alias {
196 package_aliases.insert(*package_id, package_alias.clone());
197 }
198 }
199
200 current_package_id = Some(package_store.insert(unit));
201 package_store
202 } else {
203 let mut package_store = PackageStore::new(compile::core());
204 let std_unit = compile::std(&package_store, actual_capabilities);
205 package_store.insert(std_unit);
206 package_store
207 };
208
209 Self {
210 package_store,
211 current_package_id,
212 dependencies: package_aliases,
213 }
214 }
215}
216
217impl Lookup for Compilation {
218 fn get_ty(&self, _: ast::NodeId) -> Option<&ty::Ty> {
219 unimplemented!("Not needed for docs generation")
220 }
221
222 fn get_res(&self, _: ast::NodeId) -> Option<&resolve::Res> {
223 unimplemented!("Not needed for docs generation")
224 }
225
226 fn resolve_item_relative_to_user_package(
227 &self,
228 _: &hir::ItemId,
229 ) -> (&hir::Item, &hir::Package, hir::ItemId) {
230 unimplemented!("Not needed for docs generation")
231 }
232
233 /// Returns the hir `Item` node referred to by `res`.
234 /// `Res`s can resolve to external packages, and the references
235 /// are relative, so here we also need the
236 /// local `PackageId` that the `res` itself came from.
237 fn resolve_item_res(
238 &self,
239 local_package_id: PackageId,
240 res: &hir::Res,
241 ) -> (&hir::Item, hir::ItemId) {
242 match res {
243 hir::Res::Item(item_id) => {
244 let (item, _, resolved_item_id) = self.resolve_item(local_package_id, item_id);
245 (item, resolved_item_id)
246 }
247 _ => panic!("expected to find item"),
248 }
249 }
250
251 /// Returns the hir `Item` node referred to by `item_id`.
252 /// `ItemId`s can refer to external packages, and the references
253 /// are relative, so here we also need the local `PackageId`
254 /// that the `ItemId` originates from.
255 fn resolve_item(
256 &self,
257 local_package_id: PackageId,
258 item_id: &hir::ItemId,
259 ) -> (&hir::Item, &hir::Package, hir::ItemId) {
260 // If the `ItemId` contains a package id, use that.
261 // Lack of a package id means the item is in the
262 // same package as the one this `ItemId` reference
263 // came from. So use the local package id passed in.
264 let package_id = item_id.package.unwrap_or(local_package_id);
265 let package = &self
266 .package_store
267 .get(package_id)
268 .expect("package should exist in store")
269 .package;
270 (
271 package
272 .items
273 .get(item_id.item)
274 .expect("item id should exist"),
275 package,
276 hir::ItemId {
277 package: Some(package_id),
278 item: item_id.item,
279 },
280 )
281 }
282}
283
284/// Generates and returns documentation files for the standard library
285/// and additional sources (if specified.)
286#[must_use]
287pub fn generate_docs(
288 additional_sources: Option<(PackageStore, &Dependencies, SourceMap)>,
289 capabilities: Option<TargetCapabilityFlags>,
290 language_features: Option<LanguageFeatures>,
291) -> Files {
292 // Capabilities should default to all capabilities for documentation generation.
293 let capabilities = Some(capabilities.unwrap_or(TargetCapabilityFlags::all()));
294 let compilation = Compilation::new(additional_sources, capabilities, language_features);
295 let mut files: FilesWithMetadata = vec![];
296
297 let display = &CodeDisplay {
298 compilation: &compilation,
299 };
300
301 let mut toc: ToC = FxHashMap::default();
302
303 for (package_id, unit) in &compilation.package_store {
304 let is_current_package = compilation.current_package_id == Some(package_id);
305 let package_kind;
306 if package_id == PackageId::CORE {
307 // Core package is always included in the compilation.
308 package_kind = PackageKind::Core;
309 } else if package_id == 1.into() {
310 // Standard package is currently always included, but this isn't enforced by the compiler.
311 package_kind = PackageKind::StandardLibrary;
312 } else if is_current_package {
313 // This package could be user code if current package is specified.
314 package_kind = PackageKind::UserCode;
315 } else if let Some(alias) = compilation.dependencies.get(&package_id) {
316 // This is a direct dependency of the user code.
317 package_kind = PackageKind::AliasedPackage(alias.to_string());
318 } else {
319 // This is not a package user can access (an indirect dependency).
320 continue;
321 }
322
323 let package = &unit.package;
324 for (_, item) in &package.items {
325 if let Some((ns, metadata)) = generate_doc_for_item(
326 package_id,
327 package,
328 package_kind.clone(),
329 is_current_package,
330 item,
331 display,
332 &mut files,
333 ) {
334 toc.entry(ns).or_default().push(metadata);
335 }
336 }
337 }
338
339 // Generate Overview files for each namespace
340 for (ns, items) in &mut toc {
341 generate_index_file(&mut files, ns, items);
342 }
343
344 generate_top_index(&mut files, &mut toc);
345
346 // We want to sort documentation files in a meaningful way.
347 // First, we want to put files for the current project, if it exists.
348 // Then we want to put explicit dependencies of the current project, if they exist.
349 // Then we want to add built-in std package. And finally built-in core package.
350 // Namespaces within packages should be sorted alphabetically and
351 // items with a namespace should be also sorted alphabetically with the index file appearing first.
352 // Also, items without any metadata (table of content) should come last.
353 files.sort_by_key(|file| {
354 let prefix = if file.0.ends_with("index.md") {
355 "0"
356 } else {
357 "1"
358 };
359 let name_key = format!("{}{}", prefix, file.1.name);
360 (file.1.package.clone(), file.1.namespace.clone(), name_key)
361 });
362
363 let mut result: Files = files
364 .into_iter()
365 .map(|(name, metadata, content)| (name, Rc::from(metadata.to_string().as_str()), content))
366 .collect();
367
368 generate_toc(&mut toc, &mut result);
369
370 result
371}
372
373fn generate_doc_for_item<'a>(
374 default_package_id: PackageId,
375 package: &'a Package,
376 package_kind: PackageKind,
377 include_internals: bool,
378 item: &'a Item,
379 display: &'a CodeDisplay,
380 files: &mut FilesWithMetadata,
381) -> Option<(Rc<str>, Rc<Metadata>)> {
382 let (true_package, true_item) = resolve_export(
383 default_package_id,
384 package,
385 include_internals,
386 item,
387 display,
388 )?;
389
390 // Get namespace for item
391 let ns = get_namespace(package, item)?;
392
393 // Add file
394 let (metadata, content) = if matches!(item.kind, ItemKind::Export(_, _)) {
395 let true_ns = get_namespace(true_package, true_item)?;
396 generate_exported_file_content(
397 package_kind.clone(),
398 &ns,
399 item,
400 display,
401 &true_ns,
402 true_item,
403 )?
404 } else {
405 generate_file_content(package_kind, &ns, item, display)?
406 };
407 let file_name = Rc::from(format!("{ns}/{}.md", metadata.name).as_str());
408 let file_content = Rc::from(content.as_str());
409 let met = Rc::from(metadata);
410 files.push((file_name, met.clone(), file_content));
411
412 Some((ns.clone(), met))
413}
414
415fn generate_file_content(
416 package_kind: PackageKind,
417 ns: &Rc<str>,
418 item: &Item,
419 display: &CodeDisplay,
420) -> Option<(Metadata, String)> {
421 let metadata = get_metadata(package_kind, ns.clone(), item, display)?;
422
423 let doc = increase_header_level(&item.doc);
424 let title = &metadata.title;
425 let fqn = &metadata.fully_qualified_name();
426 let sig = &metadata.signature;
427
428 let content = format!(
429 "# {title}
430
431Fully qualified name: {fqn}
432
433```qsharp
434{sig}
435```
436"
437 );
438
439 let content = if doc.is_empty() {
440 content
441 } else {
442 format!("{content}\n{doc}\n")
443 };
444
445 Some((metadata, content))
446}
447
448#[allow(clippy::assigning_clones)]
449fn generate_exported_file_content(
450 package_kind: PackageKind,
451 ns: &Rc<str>,
452 item: &Item,
453 display: &CodeDisplay,
454 true_ns: &Rc<str>,
455 true_item: &Item,
456) -> Option<(Metadata, String)> {
457 let mut metadata = get_metadata(package_kind.clone(), ns.clone(), item, display)?;
458
459 let doc = increase_header_level(&item.doc);
460 let title = &metadata.title;
461 let fqn = &metadata.fully_qualified_name();
462
463 // Note: we are assuming the package kind does not change
464 let true_metadata = get_metadata(package_kind, true_ns.clone(), true_item, display)?;
465 let true_fqn = true_metadata.fully_qualified_name();
466
467 let summary = format!(
468 "This is an exported item. The actual definition is found here: [{true_fqn}](xref:Qdk.{true_fqn})"
469 );
470
471 metadata.summary = summary.clone();
472
473 let content = format!(
474 "# {title}
475
476Fully qualified name: {fqn}
477
478{summary}
479"
480 );
481
482 let content = if doc.is_empty() {
483 content
484 } else {
485 format!("{content}\n{doc}\n")
486 };
487
488 Some((metadata, content))
489}
490
491fn generate_index_file(files: &mut FilesWithMetadata, ns: &Rc<str>, items: &mut Vec<Rc<Metadata>>) {
492 if items.is_empty() {
493 return;
494 }
495
496 let short_name = if ns.starts_with("Microsoft.Quantum") {
497 ns.as_ref()
498 } else {
499 ns.split('.')
500 .last()
501 .expect("Namespaces should have at least one part.")
502 };
503
504 let package_kind = items[0].package.clone();
505 let metadata = Metadata {
506 uid: format!("Qdk.{ns}-toc"),
507 title: format!("{ns} namespace"),
508 topic: "landing-page".to_string(),
509 kind: MetadataKind::TableOfContents,
510 package: package_kind,
511 namespace: ns.clone(),
512 name: "Overview".into(),
513 summary: format!("Table of contents for the Q# {short_name} namespace"),
514 signature: String::new(),
515 };
516
517 items.sort_by_key(|item| item.name.clone());
518 let content = items
519 .iter()
520 .map(|item| {
521 format!(
522 "| [{}](xref:Qdk.{}) | {} |",
523 item.name,
524 item.fully_qualified_name(),
525 item.summary
526 )
527 })
528 .collect::<Vec<_>>()
529 .join("\n");
530
531 let content = format!(
532 "# {ns}
533
534The {ns} namespace contains the following items:
535
536| Name | Description |
537|------|-------------|
538{content}
539",
540 );
541
542 let rc_met = Rc::from(metadata);
543 items.insert(0, rc_met.clone());
544
545 let file_name = Rc::from(format!("{ns}/index.md").as_str());
546 let file_content = Rc::from(content.as_str());
547 files.push((file_name, rc_met, file_content));
548}
549
550fn generate_top_index(files: &mut FilesWithMetadata, toc: &mut ToC) {
551 let empty_ns: Rc<str> = Rc::from(String::new().as_str());
552 let metadata = Metadata {
553 uid: "Microsoft.Quantum.apiref-toc".to_string(),
554 title: "Q# standard libraries for the Azure Quantum Development Kit".to_string(),
555 topic: "landing-page".to_string(),
556 kind: MetadataKind::TableOfContents,
557 package: PackageKind::StandardLibrary,
558 namespace: empty_ns.clone(),
559 name: "Overview".into(),
560 summary:
561 "Table of contents for the Q# standard libraries for Azure Quantum Development Kit"
562 .to_string(),
563 signature: String::new(),
564 };
565
566 let contents = table_of_contents();
567
568 files.push((Rc::from("index.md"), Rc::from(metadata), Rc::from(contents)));
569
570 toc.insert(empty_ns, vec![]);
571}
572
573/// Generates the Table of Contents file, toc.yml
574fn generate_toc(map: &mut ToC, files: &mut Files) {
575 let header = "
576# This file is automatically generated.
577# Please do not modify this file manually, or your changes will be lost when
578# documentation is rebuilt.";
579 let mut table = map
580 .iter_mut()
581 .map(|(namespace, items)| {
582 if namespace.is_empty() {
583 let content = "- items:
584 name: Overview
585 uid: Microsoft.Quantum.apiref-toc";
586 (namespace, content.to_string())
587 } else {
588 let items_str = items
589 .iter()
590 .map(|item| format!(" - {{name: {}, uid: {}}}", item.name, item.uid))
591 .collect::<Vec<String>>()
592 .join("\n");
593 let content =
594 format!("- items:\n{items_str}\n name: {namespace}\n uid: Qdk.{namespace}");
595 (namespace, content)
596 }
597 })
598 .collect::<Vec<_>>();
599
600 table.sort_unstable_by_key(|(n, _)| {
601 // Ensures that the Microsoft.Quantum.Unstable namespaces are listed last.
602 if n.starts_with("Microsoft.Quantum.Unstable") {
603 format!("1{n}")
604 } else {
605 format!("0{n}")
606 }
607 });
608 let table = table
609 .into_iter()
610 .map(|(_, c)| c)
611 .collect::<Vec<_>>()
612 .join("\n");
613 let content = format!("{header}\n{table}");
614 let content = content.as_str();
615
616 let file_name = Rc::from("toc.yml");
617 let file_metadata = Rc::from("");
618 let file_content = Rc::from(content);
619 files.push((file_name, file_metadata, file_content));
620}
621
622fn get_namespace(package: &Package, item: &Item) -> Option<Rc<str>> {
623 let local_id = item.parent?;
624 let parent = package
625 .items
626 .get(local_id)
627 .expect("Could not resolve parent item id");
628 let ItemKind::Namespace(name, _) = &parent.kind else {
629 return None;
630 };
631 if name.starts_with("QIR") {
632 None // We ignore "QIR" namespaces
633 } else {
634 Some(name.name())
635 }
636}
637
638// Recursively resolves export items until it can find the root definition.
639// Returns the package and item of the root definition for an item.
640// If given the root definition, it will return the same item.
641fn resolve_export<'a>(
642 default_package_id: PackageId,
643 package: &'a Package,
644 include_internals: bool,
645 item: &'a Item,
646 display: &'a CodeDisplay,
647) -> Option<(&'a Package, &'a Item)> {
648 // Filter out items that are not visible or are namespaces
649 if !include_internals && (item.visibility == Visibility::Internal) {
650 return None;
651 }
652 if matches!(item.kind, ItemKind::Namespace(_, _)) {
653 return None;
654 }
655 if let ItemKind::Export(_, id) = item.kind {
656 let (exported_item, exported_package, _) =
657 display.compilation.resolve_item(default_package_id, &id);
658 return resolve_export(
659 default_package_id,
660 exported_package,
661 include_internals,
662 exported_item,
663 display,
664 );
665 }
666
667 Some((package, item))
668}
669
670fn get_metadata(
671 package_kind: PackageKind,
672 ns: Rc<str>,
673 item: &Item,
674 display: &CodeDisplay,
675) -> Option<Metadata> {
676 let (name, signature, kind) = match &item.kind {
677 ItemKind::Callable(decl) => Some((
678 decl.name.name.clone(),
679 display.hir_callable_decl(decl).to_string(),
680 match &decl.kind {
681 CallableKind::Function => MetadataKind::Function,
682 CallableKind::Operation => MetadataKind::Operation,
683 },
684 )),
685 ItemKind::Ty(ident, udt) => Some((
686 ident.name.clone(),
687 display.hir_udt(udt).to_string(),
688 MetadataKind::Udt,
689 )),
690 ItemKind::Namespace(_, _) => None,
691 ItemKind::Export(name, _) => Some((
692 name.name.clone(),
693 // If we want to show docs for exports, we could do that here.
694 String::new(),
695 MetadataKind::Export,
696 )),
697 }?;
698
699 let summary = parse_doc_for_summary(&item.doc)
700 .replace("\r\n", " ")
701 .replace('\n', " ")
702 .replace('|', "\\|");
703
704 Some(Metadata {
705 uid: format!("Qdk.{ns}.{name}"),
706 title: format!("{name} {kind}"),
707 topic: "managed-reference".to_string(),
708 kind,
709 package: package_kind,
710 namespace: ns,
711 name,
712 summary,
713 signature,
714 })
715}
716