Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions source/compiler/qsc_frontend/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct CompileUnit {
pub sources: SourceMap,
pub errors: Vec<Error>,
pub dropped_names: Vec<TrackedName>,
pub dropped_spans: Vec<Span>,
}

impl CompileUnit {
Expand All @@ -59,6 +60,7 @@ impl CompileUnit {
sources: Default::default(),
errors: Default::default(),
dropped_names: Default::default(),
dropped_spans: Default::default(),
}
}

Expand Down Expand Up @@ -284,6 +286,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);
Expand Down Expand Up @@ -336,6 +339,7 @@ pub fn compile_ast(
sources,
errors,
dropped_names,
dropped_spans,
}
}

Expand Down
11 changes: 11 additions & 0 deletions source/compiler/qsc_frontend/src/compile/preprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub(crate) struct Conditional {
capabilities: TargetCapabilityFlags,
dropped_names: Vec<TrackedName>,
included_names: Vec<TrackedName>,
dropped_spans: Vec<Span>,
}

impl Conditional {
Expand All @@ -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<Span> {
std::mem::take(&mut self.dropped_spans)
}

pub(crate) fn into_names(self) -> Vec<TrackedName> {
self.dropped_names
.into_iter()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 20 additions & 1 deletion source/compiler/qsc_frontend/src/compile/preprocess/tests.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()));
Expand Down
16 changes: 16 additions & 0 deletions source/language_service/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
pub struct UnnecessaryCodeDiagnostic {
pub range: Range,
}

/// Document status is a non-user facing, info-level diagnostic meant for
Expand Down
33 changes: 32 additions & 1 deletion source/language_service/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -720,6 +729,28 @@ 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<Arc<str>, Vec<ErrorKind>>,
) {
let unit = compilation.user_unit();
let source_map = &unit.sources;
for &span in &unit.dropped_spans {
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()
.push(ErrorKind::Unnecessary(UnnecessaryCodeDiagnostic { range }));
}
}

/// Merges workspace configuration with any compilation-specific overrides.
fn merge_configurations(
compilation_overrides: &PartialConfiguration,
Expand Down
28 changes: 28 additions & 0 deletions source/language_service/src/state/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
8 changes: 6 additions & 2 deletions source/language_service/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
19 changes: 14 additions & 5 deletions source/playground/src/editor.tsx
Comment thread
sorin-bolos marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -114,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);
}

Expand Down
8 changes: 8 additions & 0 deletions source/vscode/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 27 additions & 2 deletions source/wasm/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,26 @@ serializable_type! {
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub related: Vec<Related>
pub related: Vec<Related>,
// LSP `DiagnosticTag` values, e.g. 1 = Unnecessary, 2 = Deprecated.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<u8>
},
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,
{
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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: None,
uri: None,
related: Vec::new(),
tags: vec![DIAGNOSTIC_TAG_UNNECESSARY],
}
}
}
Expand Down