microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.17.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

compiler/qsc_doc_gen/src/generate_docs.rs

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