Repository: microsoft/qsharp (language: python)

Issue:
Add documentation to attributes in Q#
**Is your feature request related to a problem? Please describe.**
We have added a couple more attributes since the last release and we plan to add a few more going forward. It would be helpful to the users to have some documentation show up when you hover over an attribute.

**Describe the solution you'd like**
The documentation strings can be hard coded into the language service, since there is no way to define custom attributes in Q#.

A screenshot showing the lack of documentation when you hover over an attribute.
![Image](https://github.com/user-attachments/assets/2ad8cf51-8744-4df3-9c03-061a0197d50d)



Discussion / hints:

Fix patch:
diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs
index f1e0958aa8..c349b203bb 100644
--- a/compiler/qsc_frontend/src/lower.rs
+++ b/compiler/qsc_frontend/src/lower.rs
@@ -29,7 +29,9 @@ use self::convert::TyConversionError;
 #[derive(Clone, Debug, Diagnostic, Error)]
 pub(super) enum Error {
     #[error("unknown attribute {0}")]
-    #[diagnostic(help("supported attributes are: EntryPoint, Config"))]
+    #[diagnostic(help(
+        "supported attributes are: EntryPoint, Config, SimulatableIntrinsic, Measurement, Reset"
+    ))]
     #[diagnostic(code("Qsc.LowerAst.UnknownAttr"))]
     UnknownAttr(String, #[label] Span),
     #[error("invalid attribute arguments: expected {0}")]
diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs
index cb42b9be3b..5c22bdd0fe 100644
--- a/compiler/qsc_hir/src/hir.rs
+++ b/compiler/qsc_hir/src/hir.rs
@@ -1344,23 +1344,42 @@ impl Display for Ident {
 /// An attribute.
 #[derive(Clone, Debug, PartialEq)]
 pub enum Attr {
-    /// Provide pre-processing information about when an item should be included in compilation.
+    /// Provides pre-processing information about when an item should be included in compilation.
     Config,
-    /// Indicates that a callable is an entry point to a program.
+    /// Indicates that the callable is the entry point to a program.
     EntryPoint,
-    /// Indicates that an item does not have an implementation available for use.
+    /// Indicates that an item is not yet implemented.
     Unimplemented,
     /// Indicates that an item should be treated as an intrinsic callable for QIR code generation
-    /// and any implementation should be ignored.
+    /// and any implementation should only be used during simulation.
     SimulatableIntrinsic,
-    /// Indicates that a callable is a measurement. This means that the operation will be marked as
+    /// Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as
     /// "irreversible" in the generated QIR, and output Result types will be moved to the arguments.
     Measurement,
-    /// Indicates that a callable is a reset. This means that the operation will be marked as
+    /// Indicates that an intrinsic callable is a reset. This means that the operation will be marked as
     /// "irreversible" in the generated QIR.
     Reset,
 }
 
+impl Attr {
+    /// Gets the string description of the attribute.
+    #[must_use]
+    pub fn description(&self) -> &'static str {
+        match self {
+            Attr::Config => "Provides pre-processing information about when an item should be included in compilation.
+
+Valid arguments are `Base`, `Adaptive`, `IntegerComputations`, `FloatingPointComputations`, `BackwardsBranching`, `HigherLevelConstructs`, `QubitReset`, and `Unrestricted`.
+
+The `not` operator is also supported to negate the attribute, e.g. `not Adaptive`.",
+            Attr::EntryPoint => "Indicates that the callable is the entry point to a program.",
+            Attr::Unimplemented => "Indicates that an item is not yet implemented.",
+            Attr::SimulatableIntrinsic => "Indicates that an item should be treated as an intrinsic callable for QIR code generation and any implementation should only be used during simulation.",
+            Attr::Measurement => "Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as \"irreversible\" in the generated QIR, and output Result types will be moved to the arguments.",
+            Attr::Reset => "Indicates that an intrinsic callable is a reset. This means that the operation will be marked as \"irreversible\" in the generated QIR.",
+        }
+    }
+}
+
 impl FromStr for Attr {
     type Err = ();
 
diff --git a/language_service/src/definition.rs b/language_service/src/definition.rs
index a32d611d44..a3ca15a334 100644
--- a/language_service/src/definition.rs
+++ b/language_service/src/definition.rs
@@ -42,6 +42,10 @@ struct DefinitionFinder<'a> {
 }
 
 impl<'a> Handler<'a> for DefinitionFinder<'a> {
+    fn at_attr_ref(&mut self, _: &'a ast::Ident) {
+        // We don't support goto def for attributes.
+    }
+
     fn at_callable_def(
         &mut self,
         _: &LocatorContext<'a>,
diff --git a/language_service/src/hover.rs b/language_service/src/hover.rs
index 8e9de3f68c..954a4faa33 100644
--- a/language_service/src/hover.rs
+++ b/language_service/src/hover.rs
@@ -10,10 +10,12 @@ use crate::protocol::Hover;
 use crate::qsc_utils::into_range;
 use qsc::ast::visit::Visitor;
 use qsc::display::{parse_doc_for_param, parse_doc_for_summary, CodeDisplay, Lookup};
+use qsc::hir::Attr;
 use qsc::line_column::{Encoding, Position, Range};
 use qsc::{ast, hir, Span};
 use std::fmt::Display;
 use std::rc::Rc;
+use std::str::FromStr;
 
 pub(crate) fn get_hover(
     compilation: &Compilation,
@@ -43,6 +45,7 @@ enum LocalKind {
     LambdaParam,
     Local,
 }
+
 struct HoverGenerator<'a> {
     position_encoding: Encoding,
     hover: Option<Hover>,
@@ -51,6 +54,18 @@ struct HoverGenerator<'a> {
 }
 
 impl<'a> Handler<'a> for HoverGenerator<'a> {
+    fn at_attr_ref(&mut self, name: &'a ast::Ident) {
+        let description = match Attr::from_str(&name.name) {
+            Ok(attr) => attr.description(),
+            Err(()) => return, // No hover information for unsupported attributes.
+        };
+
+        self.hover = Some(Hover {
+            contents: format!("attribute ```{}```\n\n{}", name.name, description),
+            span: self.range(name.span),
+        });
+    }
+
     fn at_callable_def(
         &mut self,
         context: &LocatorContext<'a>,
diff --git a/language_service/src/name_locator.rs b/language_service/src/name_locator.rs
index 22d3a8f2cd..5243b56f1f 100644
--- a/language_service/src/name_locator.rs
+++ b/language_service/src/name_locator.rs
@@ -5,19 +5,23 @@ use std::mem::replace;
 use std::rc::Rc;
 
 use crate::compilation::Compilation;
-use qsc::ast::visit::{walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor};
+use qsc::ast::visit::{
+    walk_attr, walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor,
+};
 use qsc::ast::{FieldAccess, Idents, PathKind};
 use qsc::display::Lookup;
 use qsc::{ast, hir, resolve};
 
-#[allow(unused_variables)]
 pub(crate) trait Handler<'package> {
+    fn at_attr_ref(&mut self, name: &'package ast::Ident);
+
     fn at_callable_def(
         &mut self,
         context: &LocatorContext<'package>,
         name: &'package ast::Ident,
         decl: &'package ast::CallableDecl,
     );
+
     fn at_callable_ref(
         &mut self,
         path: &'package ast::Path,
@@ -166,6 +170,13 @@ impl<'inner, 'package, T: Handler<'package>> Locator<'inner, 'package, T> {
 }
 
 impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inner, 'package, T> {
+    fn visit_attr(&mut self, attr: &'package ast::Attr) {
+        if attr.name.span.contains(self.offset) {
+            self.inner.at_attr_ref(&attr.name);
+        }
+        walk_attr(self, attr);
+    }
+
     fn visit_namespace(&mut self, namespace: &'package ast::Namespace) {
         if namespace.span.contains(self.offset) {
             self.context.current_namespace = namespace.name.full_name();
@@ -177,6 +188,7 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne
     fn visit_item(&mut self, item: &'package ast::Item) {
         if item.span.contains(self.offset) {
             let context = replace(&mut self.context.current_item_doc, item.doc.clone());
+            item.attrs.iter().for_each(|a| self.visit_attr(a));
             match &*item.kind {
                 ast::ItemKind::Callable(decl) => {
                     if decl.name.span.touches(self.offset) {
diff --git a/language_service/src/references.rs b/language_service/src/references.rs
index 59026da5b3..4f7bc4cfc6 100644
--- a/language_service/src/references.rs
+++ b/language_service/src/references.rs
@@ -52,6 +52,10 @@ struct NameHandler<'a> {
 }
 
 impl<'a> Handler<'a> for NameHandler<'a> {
+    fn at_attr_ref(&mut self, _: &'a ast::Ident) {
+        // We don't support find all refs for attributes.
+    }
+
     fn at_callable_def(
         &mut self,
         _: &LocatorContext<'a>,
diff --git a/language_service/src/rename.rs b/language_service/src/rename.rs
index 5df6aa82c9..8a963816fb 100644
--- a/language_service/src/rename.rs
+++ b/language_service/src/rename.rs
@@ -152,6 +152,10 @@ impl<'a> Rename<'a> {
 }
 
 impl<'a> Handler<'a> for Rename<'a> {
+    fn at_attr_ref(&mut self, _: &'a ast::Ident) {
+        // We don't support renaming attributes.
+    }
+
     fn at_callable_def(
         &mut self,
         _: &LocatorContext<'a>,


Test patch:
diff --git a/language_service/src/hover/tests.rs b/language_service/src/hover/tests.rs
index faa281bb73..361d0f124a 100644
--- a/language_service/src/hover/tests.rs
+++ b/language_service/src/hover/tests.rs
@@ -43,6 +43,42 @@ fn check_notebook_none(cells_with_markers: &[(&str, &str)]) {
     assert!(actual.is_none());
 }
 
+#[test]
+fn attr() {
+    check(
+        indoc! {r#"
+        namespace Test {
+            @◉Entr↘yPoint◉()
+            operation Bar() : Unit {}
+        }
+    "#},
+        &expect![[r#"
+            attribute ```EntryPoint```
+
+            Indicates that the callable is the entry point to a program."#]],
+    );
+}
+
+#[test]
+fn attr_with_arg() {
+    check(
+        indoc! {r#"
+        namespace Test {
+            @◉Con↘fig◉(BackwardsBranching)
+            operation Bar() : Unit {}
+        }
+    "#},
+        &expect![[r#"
+            attribute ```Config```
+
+            Provides pre-processing information about when an item should be included in compilation.
+
+            Valid arguments are `Base`, `Adaptive`, `IntegerComputations`, `FloatingPointComputations`, `BackwardsBranching`, `HigherLevelConstructs`, `QubitReset`, and `Unrestricted`.
+
+            The `not` operator is also supported to negate the attribute, e.g. `not Adaptive`."#]],
+    );
+}
+
 #[test]
 fn callable_unit_types() {
     check(