microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.18.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/compiler/qsc_doc_gen/src/generate_docs.rs

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