From 0c62dd0c464c2823a098fc9ee0eb7d34e5c40152 Mon Sep 17 00:00:00 2001 From: sorin-bolos Date: Fri, 5 Jun 2026 12:39:23 +0300 Subject: [PATCH 1/3] Gray out excluded code --- source/compiler/qsc_frontend/src/compile.rs | 7 ++++ .../qsc_frontend/src/compile/preprocess.rs | 11 ++++++ .../src/compile/preprocess/tests.rs | 21 +++++++++++- source/language_service/src/protocol.rs | 16 +++++++++ source/language_service/src/state.rs | 34 ++++++++++++++++++- source/language_service/src/state/tests.rs | 28 +++++++++++++++ source/language_service/src/tests.rs | 8 +++-- source/playground/src/editor.tsx | 5 +++ source/vscode/src/common.ts | 8 +++++ source/wasm/src/diagnostic.rs | 29 ++++++++++++++-- 10 files changed, 161 insertions(+), 6 deletions(-) diff --git a/source/compiler/qsc_frontend/src/compile.rs b/source/compiler/qsc_frontend/src/compile.rs index e21356241c..873d124af1 100644 --- a/source/compiler/qsc_frontend/src/compile.rs +++ b/source/compiler/qsc_frontend/src/compile.rs @@ -47,6 +47,10 @@ pub struct CompileUnit { pub sources: SourceMap, pub errors: Vec, pub dropped_names: Vec, + /// Spans of items that were excluded from this compilation because their + /// `@Config` attributes did not match the current target capabilities. + /// These are reported to the editor so the excluded code can be greyed out. + pub dropped_spans: Vec, } impl CompileUnit { @@ -59,6 +63,7 @@ impl CompileUnit { sources: Default::default(), errors: Default::default(), dropped_names: Default::default(), + dropped_spans: Default::default(), } } @@ -284,6 +289,7 @@ pub fn compile_ast( ) -> CompileUnit { let mut cond_compile = preprocess::Conditional::new(capabilities); cond_compile.visit_package(&mut ast_package); + let dropped_spans = cond_compile.take_dropped_spans(); let dropped_names = cond_compile.into_names(); let mut remove_spans = preprocess::RemoveCircuitSpans::new(&sources); @@ -336,6 +342,7 @@ pub fn compile_ast( sources, errors, dropped_names, + dropped_spans, } } diff --git a/source/compiler/qsc_frontend/src/compile/preprocess.rs b/source/compiler/qsc_frontend/src/compile/preprocess.rs index 8ec7d582ac..95deb2c3c4 100644 --- a/source/compiler/qsc_frontend/src/compile/preprocess.rs +++ b/source/compiler/qsc_frontend/src/compile/preprocess.rs @@ -56,6 +56,7 @@ pub(crate) struct Conditional { capabilities: TargetCapabilityFlags, dropped_names: Vec, included_names: Vec, + dropped_spans: Vec, } impl Conditional { @@ -64,9 +65,17 @@ impl Conditional { capabilities, dropped_names: Vec::new(), included_names: Vec::new(), + dropped_spans: Vec::new(), } } + /// Takes the spans of any items that were dropped from the compilation + /// because they did not match the current target capabilities. These are + /// reported to the editor so that the excluded code can be greyed out. + pub(crate) fn take_dropped_spans(&mut self) -> Vec { + std::mem::take(&mut self.dropped_spans) + } + pub(crate) fn into_names(self) -> Vec { self.dropped_names .into_iter() @@ -97,6 +106,7 @@ impl MutVisitor for Conditional { } Some(item.clone()) } else { + self.dropped_spans.push(item.span); match item.kind.as_ref() { ItemKind::Callable(callable) => { self.dropped_names.push(TrackedName { @@ -134,6 +144,7 @@ impl MutVisitor for Conditional { _ => {} } } else { + self.dropped_spans.push(item.span); match item.kind.as_ref() { ItemKind::Callable(callable) => { self.dropped_names.push(TrackedName { diff --git a/source/compiler/qsc_frontend/src/compile/preprocess/tests.rs b/source/compiler/qsc_frontend/src/compile/preprocess/tests.rs index 0dde411928..89d778029e 100644 --- a/source/compiler/qsc_frontend/src/compile/preprocess/tests.rs +++ b/source/compiler/qsc_frontend/src/compile/preprocess/tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::compile::preprocess::RemoveCircuitSpans; +use crate::compile::preprocess::{Conditional, RemoveCircuitSpans}; use crate::compile::{SourceMap, parse_all}; use qsc_ast::ast::{ Attr, CallableBody, CallableDecl, Expr, ExprKind, Ident, NodeId, Path, PathKind, @@ -103,6 +103,25 @@ fn find_callable<'a>(package: &'a Package, name: &str) -> &'a CallableDecl { .unwrap_or_else(|| panic!("{name} callable not found")) } +#[test] +fn dropped_item_spans_cover_the_excluded_item() { + let source = "namespace Test { @Config(Adaptive) operation Excluded() : Unit {} operation Included() : Unit {} }"; + let sources = SourceMap::new([(Arc::from("test.qs"), Arc::from(source))], None); + let (mut package, errs) = parse_all(&sources, LanguageFeatures::default()); + assert!(errs.is_empty(), "{errs:?}"); + + // Base profile (empty capabilities) drops the `@Config(Adaptive)` item. + let mut cond = Conditional::new(TargetCapabilityFlags::empty()); + cond.visit_package(&mut package); + let dropped_spans = cond.take_dropped_spans(); + + assert_eq!(dropped_spans.len(), 1, "exactly one item should be dropped"); + let span = dropped_spans[0]; + // The span should start at the `@Config` attribute and cover the operation. + let excluded = &source[span.lo as usize..span.hi as usize]; + assert_eq!(excluded, "@Config(Adaptive) operation Excluded() : Unit {}"); +} + #[test] fn no_attrs_matches() { assert!(matches_config(&[], TargetCapabilityFlags::empty())); diff --git a/source/language_service/src/protocol.rs b/source/language_service/src/protocol.rs index 511e962169..a8cfc2da8f 100644 --- a/source/language_service/src/protocol.rs +++ b/source/language_service/src/protocol.rs @@ -30,6 +30,22 @@ pub enum ErrorKind { #[error(transparent)] #[diagnostic(transparent)] DocumentStatus(#[from] DocumentStatusDiagnostic), + #[error(transparent)] + #[diagnostic(transparent)] + Unnecessary(#[from] UnnecessaryCodeDiagnostic), +} + +/// Marks a region of code that is excluded from the current compilation, e.g. +/// an item whose `@Config` attribute does not match the current target profile. +/// This is reported to the editor as a hint carrying the `Unnecessary` +/// diagnostic tag so that the excluded code can be displayed as greyed out. +#[derive(Clone, Debug, Diagnostic, Error)] +#[error( + "this code is not included in the current compilation because it does not apply to the current target profile" +)] +#[diagnostic(severity(info), code("Qdk.LanguageServer.UnnecessaryCode"))] +pub struct UnnecessaryCodeDiagnostic { + pub range: Range, } /// Document status is a non-user facing, info-level diagnostic meant for diff --git a/source/language_service/src/state.rs b/source/language_service/src/state.rs index 4b7c5cc3f3..885cf86ca3 100644 --- a/source/language_service/src/state.rs +++ b/source/language_service/src/state.rs @@ -4,7 +4,8 @@ #[cfg(test)] mod tests; -use crate::protocol::{DocumentStatusDiagnostic, TestCallable}; +use crate::protocol::{DocumentStatusDiagnostic, TestCallable, UnnecessaryCodeDiagnostic}; +use crate::qsc_utils::into_range; use super::compilation::Compilation; use super::protocol::{ @@ -492,6 +493,14 @@ impl<'a> CompilationStateUpdater<'a> { &compilation.0.project_errors, ); + // Report any code that was excluded from the compilation (e.g. via + // `@Config` attributes) so the editor can grey it out. + add_unnecessary_code_diagnostics( + &compilation.0, + self.position_encoding, + &mut compilation_diags_by_doc, + ); + if self.configuration.dev_diagnostics { // Add the document status diagnostic for all open documents too for (uri, open_document) in &state.open_documents { @@ -720,6 +729,29 @@ fn map_errors_to_docs( map } +/// Adds hint-level diagnostics for any items that were excluded from the +/// compilation because their `@Config` attributes did not match the current +/// target profile. These carry the `Unnecessary` diagnostic tag so editors +/// can display the excluded code as greyed out. +fn add_unnecessary_code_diagnostics( + compilation: &Compilation, + encoding: Encoding, + map: &mut FxHashMap, Vec>, +) { + let unit = compilation.user_unit(); + let source_map = &unit.sources; + for &span in &unit.dropped_spans { + // Skip any span we can't resolve to a source rather than panicking. + let Some(source) = source_map.find_by_offset(span.lo) else { + continue; + }; + let range = into_range(encoding, span, source_map); + map.entry(source.name.clone()) + .or_default() + .push(ErrorKind::Unnecessary(UnnecessaryCodeDiagnostic { range })); + } +} + /// Merges workspace configuration with any compilation-specific overrides. fn merge_configurations( compilation_overrides: &PartialConfiguration, diff --git a/source/language_service/src/state/tests.rs b/source/language_service/src/state/tests.rs index b5f8a0a2ac..65bf8ffed4 100644 --- a/source/language_service/src/state/tests.rs +++ b/source/language_service/src/state/tests.rs @@ -78,6 +78,34 @@ async fn clear_error() { ); } +#[tokio::test] +async fn conditionally_excluded_code_reported_as_unnecessary() { + let errors = RefCell::new(Vec::new()); + let test_cases = RefCell::new(Vec::new()); + let mut updater = new_updater(&errors, &test_cases); + + // The default target profile is Unrestricted, so the `@Config(Base)` item + // is excluded from the compilation and should be reported as unnecessary. + updater + .update_document( + "single/foo.qs", + 1, + "namespace Foo { operation Main() : Unit {} @Config(Base) operation Excluded() : Unit {} }", + "qsharp", + ) + .await; + + expect_errors( + &errors, + &expect![[r#" + [ + uri: "single/foo.qs" version: Some(1) errors: [ + this code is not included in the current compilation because it does not apply to the current target profile + ], + ]"#]], + ); +} + #[tokio::test] async fn close_last_doc_in_project() { let received_errors = RefCell::new(Vec::new()); diff --git a/source/language_service/src/tests.rs b/source/language_service/src/tests.rs index b4392f60eb..e703e26491 100644 --- a/source/language_service/src/tests.rs +++ b/source/language_service/src/tests.rs @@ -280,11 +280,15 @@ fn create_update_worker<'a>( |update: DiagnosticUpdate| { let project_errors = update.errors.iter().filter_map(|error| match error { ErrorKind::Project(error) => Some(error.clone()), - ErrorKind::Compile(_) | ErrorKind::DocumentStatus { .. } => None, + ErrorKind::Compile(_) + | ErrorKind::DocumentStatus { .. } + | ErrorKind::Unnecessary(_) => None, }); let compile_errors = update.errors.iter().filter_map(|error| match error { ErrorKind::Compile(error) => Some(error.error().clone()), - ErrorKind::Project(_) | ErrorKind::DocumentStatus { .. } => None, + ErrorKind::Project(_) + | ErrorKind::DocumentStatus { .. } + | ErrorKind::Unnecessary(_) => None, }); let mut v = received_errors.borrow_mut(); diff --git a/source/playground/src/editor.tsx b/source/playground/src/editor.tsx index 28ef7323b1..f9d651abd9 100644 --- a/source/playground/src/editor.tsx +++ b/source/playground/src/editor.tsx @@ -39,12 +39,17 @@ function VSDiagsToMarkers(errors: VSDiagnostic[]): monaco.editor.IMarkerData[] { case "info": severity = monaco.MarkerSeverity.Info; break; + case "hint": + severity = monaco.MarkerSeverity.Hint; + break; } const marker: monaco.editor.IMarkerData = { ...lsRangeToMonacoRange(err.range), severity, message: err.message, + // LSP DiagnosticTag values match monaco's MarkerTag (1 = Unnecessary). + tags: err.tags as monaco.MarkerTag[] | undefined, relatedInformation: err.related?.map((r) => { const range = lsRangeToMonacoRange(r.location.span); return { diff --git a/source/vscode/src/common.ts b/source/vscode/src/common.ts index 2c2ca278be..2a0e33a18d 100644 --- a/source/vscode/src/common.ts +++ b/source/vscode/src/common.ts @@ -123,12 +123,20 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { case "info": severity = vscode.DiagnosticSeverity.Information; break; + case "hint": + severity = vscode.DiagnosticSeverity.Hint; + break; } const vscodeDiagnostic = new vscode.Diagnostic( toVsCodeRange(d.range), d.message, severity, ); + if (d.tags) { + // LSP DiagnosticTag values: 1 = Unnecessary, 2 = Deprecated. + // VS Code's DiagnosticTag enum uses the same numeric values. + vscodeDiagnostic.tags = d.tags as vscode.DiagnosticTag[]; + } if (d.uri && d.code) { vscodeDiagnostic.code = { value: d.code, diff --git a/source/wasm/src/diagnostic.rs b/source/wasm/src/diagnostic.rs index 666226bcec..fa0eddb83e 100644 --- a/source/wasm/src/diagnostic.rs +++ b/source/wasm/src/diagnostic.rs @@ -23,18 +23,26 @@ serializable_type! { #[serde(skip_serializing_if = "Option::is_none")] pub uri: Option, #[serde(skip_serializing_if = "Vec::is_empty")] - pub related: Vec + pub related: Vec, + // LSP `DiagnosticTag` values, e.g. 1 = Unnecessary, 2 = Deprecated. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tags: Vec }, r#"export interface VSDiagnostic { range: IRange, message: string; - severity: "error" | "warning" | "info" + severity: "error" | "warning" | "info" | "hint" code?: string; uri?: string; related?: IRelatedInformation[]; + tags?: number[]; }"# } +/// LSP `DiagnosticTag` value indicating unused or unnecessary code, which +/// editors typically render as greyed out. +const DIAGNOSTIC_TAG_UNNECESSARY: u8 = 1; + serializable_type! { Related, { @@ -85,6 +93,7 @@ impl VSDiagnostic { e @ qsls::protocol::ErrorKind::DocumentStatus { .. } => { Self::new(Vec::new(), source_name, e) } + qsls::protocol::ErrorKind::Unnecessary(d) => Self::unnecessary(d), } } @@ -167,6 +176,22 @@ impl VSDiagnostic { code, uri, related, + tags: Vec::new(), + } + } + + /// Creates a [`VSDiagnostic`] for a region of code that is excluded from the + /// current compilation. It is reported with `hint` severity and the + /// `Unnecessary` tag so that editors render the excluded code as greyed out. + fn unnecessary(d: &qsls::protocol::UnnecessaryCodeDiagnostic) -> Self { + Self { + range: d.range.into(), + message: d.to_string(), + severity: "hint".to_string(), + code: d.code().map(|c| c.to_string()), + uri: None, + related: Vec::new(), + tags: vec![DIAGNOSTIC_TAG_UNNECESSARY], } } } From 413a0fa85f2dc1ffc28fc3a54519efd9ad6ea0b1 Mon Sep 17 00:00:00 2001 From: sorin-bolos Date: Fri, 5 Jun 2026 12:51:13 +0300 Subject: [PATCH 2/3] Remove comment --- source/compiler/qsc_frontend/src/compile.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/compiler/qsc_frontend/src/compile.rs b/source/compiler/qsc_frontend/src/compile.rs index 873d124af1..34dd543b73 100644 --- a/source/compiler/qsc_frontend/src/compile.rs +++ b/source/compiler/qsc_frontend/src/compile.rs @@ -47,9 +47,6 @@ pub struct CompileUnit { pub sources: SourceMap, pub errors: Vec, pub dropped_names: Vec, - /// Spans of items that were excluded from this compilation because their - /// `@Config` attributes did not match the current target capabilities. - /// These are reported to the editor so the excluded code can be greyed out. pub dropped_spans: Vec, } From 0f6b54d7be41cd43f2135c2b7a82119245803fee Mon Sep 17 00:00:00 2001 From: sorin-bolos Date: Sat, 6 Jun 2026 10:21:34 +0300 Subject: [PATCH 3/3] Do not show hint in playground --- source/language_service/src/protocol.rs | 2 +- source/language_service/src/state.rs | 7 +++---- source/playground/src/editor.tsx | 14 +++++++++----- source/wasm/src/diagnostic.rs | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/source/language_service/src/protocol.rs b/source/language_service/src/protocol.rs index a8cfc2da8f..3b524425c9 100644 --- a/source/language_service/src/protocol.rs +++ b/source/language_service/src/protocol.rs @@ -43,7 +43,7 @@ pub enum ErrorKind { #[error( "this code is not included in the current compilation because it does not apply to the current target profile" )] -#[diagnostic(severity(info), code("Qdk.LanguageServer.UnnecessaryCode"))] +#[diagnostic(severity(info))] pub struct UnnecessaryCodeDiagnostic { pub range: Range, } diff --git a/source/language_service/src/state.rs b/source/language_service/src/state.rs index 885cf86ca3..df57313170 100644 --- a/source/language_service/src/state.rs +++ b/source/language_service/src/state.rs @@ -741,10 +741,9 @@ fn add_unnecessary_code_diagnostics( let unit = compilation.user_unit(); let source_map = &unit.sources; for &span in &unit.dropped_spans { - // Skip any span we can't resolve to a source rather than panicking. - let Some(source) = source_map.find_by_offset(span.lo) else { - continue; - }; + let source = source_map + .find_by_offset(span.lo) + .expect("dropped span should resolve to a source"); let range = into_range(encoding, span, source_map); map.entry(source.name.clone()) .or_default() diff --git a/source/playground/src/editor.tsx b/source/playground/src/editor.tsx index f9d651abd9..37da22d38b 100644 --- a/source/playground/src/editor.tsx +++ b/source/playground/src/editor.tsx @@ -119,11 +119,15 @@ export function Editor(props: { const markers = VSDiagsToMarkers(errs); monaco.editor.setModelMarkers(model, "qsharp", markers); - const errList = markers.map((err) => ({ - location: `main.qs@(${err.startLineNumber},${err.startColumn})`, - severity: err.severity, - msg: err.message.split("\n\n"), - })); + const errList = markers + // Hints (e.g. grayed-out excluded code) are shown inline only, not in the + // diagnostics list below the editor, mirroring VS Code's Problems view. + .filter((err) => err.severity !== monaco.MarkerSeverity.Hint) + .map((err) => ({ + location: `main.qs@(${err.startLineNumber},${err.startColumn})`, + severity: err.severity, + msg: err.message.split("\n\n"), + })); setErrors(errList); } diff --git a/source/wasm/src/diagnostic.rs b/source/wasm/src/diagnostic.rs index fa0eddb83e..e87e279f91 100644 --- a/source/wasm/src/diagnostic.rs +++ b/source/wasm/src/diagnostic.rs @@ -188,7 +188,7 @@ impl VSDiagnostic { range: d.range.into(), message: d.to_string(), severity: "hint".to_string(), - code: d.code().map(|c| c.to_string()), + code: None, uri: None, related: Vec::new(), tags: vec![DIAGNOSTIC_TAG_UNNECESSARY],