microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-2145

Branches

Tags

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

Clone

HTTPS

Download ZIP

compiler/qsc_doc_gen/src/generate_docs.rs

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