diff --git a/compiler/qsc/src/compile.rs b/compiler/qsc/src/compile.rs index 85d9958e89..246238d232 100644 --- a/compiler/qsc/src/compile.rs +++ b/compiler/qsc/src/compile.rs @@ -33,6 +33,7 @@ pub enum ErrorKind { Lint(#[from] qsc_linter::Lint), } +/// Compiles a package from its AST representation. #[must_use] #[allow(clippy::module_name_repetitions)] pub fn compile_ast( @@ -54,6 +55,7 @@ pub fn compile_ast( process_compile_unit(store, package_type, capabilities, unit) } +/// Compiles a package from its source representation. #[must_use] pub fn compile( store: &PackageStore, diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs index 29f7400b19..7fa37c6307 100644 --- a/compiler/qsc_ast/src/ast.rs +++ b/compiler/qsc_ast/src/ast.rs @@ -1406,7 +1406,7 @@ impl Idents { self.into() } - /// the conjoined span of all idents in the `VecIdent` + /// the conjoined span of all idents in the `Idents` #[must_use] pub fn span(&self) -> Span { Span { diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs index 5263f05b96..d4ac5c13b6 100644 --- a/compiler/qsc_eval/src/tests.rs +++ b/compiler/qsc_eval/src/tests.rs @@ -3689,6 +3689,8 @@ fn partial_eval_simple_stmt() { Package: Entry Expression: 0 Items: + Item 0 [12-12] (Public): + Namespace (Ident 0 [12-12] "test"): Blocks: Block 0 [0-11] [Type Unit]: 0 @@ -3722,6 +3724,8 @@ fn partial_eval_stmt_with_bound_variable() { Package: Entry Expression: 0 Items: + Item 0 [20-20] (Public): + Namespace (Ident 1 [20-20] "test"): Blocks: Block 0 [0-19] [Type Unit]: 0 @@ -3758,6 +3762,8 @@ fn partial_eval_stmt_with_mutable_variable_update() { Package: Entry Expression: 0 Items: + Item 0 [45-45] (Public): + Namespace (Ident 1 [45-45] "test"): Blocks: Block 0 [0-44] [Type Unit]: 0 @@ -3807,6 +3813,8 @@ fn partial_eval_stmt_with_mutable_variable_update_out_of_order_works() { Package: Entry Expression: 0 Items: + Item 0 [45-45] (Public): + Namespace (Ident 1 [45-45] "test"): Blocks: Block 0 [0-44] [Type Unit]: 0 @@ -3856,6 +3864,8 @@ fn partial_eval_stmt_with_mutable_variable_update_repeat_stmts_works() { Package: Entry Expression: 0 Items: + Item 0 [45-45] (Public): + Namespace (Ident 1 [45-45] "test"): Blocks: Block 0 [0-44] [Type Unit]: 0 @@ -3905,6 +3915,8 @@ fn partial_eval_stmt_with_bool_short_circuit() { Package: Entry Expression: 0 Items: + Item 0 [35-35] (Public): + Namespace (Ident 1 [35-35] "test"): Blocks: Block 0 [0-34] [Type Unit]: 0 @@ -3945,6 +3957,8 @@ fn partial_eval_stmt_with_bool_no_short_circuit() { Package: Entry Expression: 0 Items: + Item 0 [35-35] (Public): + Namespace (Ident 1 [35-35] "test"): Blocks: Block 0 [0-34] [Type Unit]: 0 @@ -3985,6 +3999,8 @@ fn partial_eval_stmt_with_loop() { Package: Entry Expression: 0 Items: + Item 0 [53-53] (Public): + Namespace (Ident 1 [53-53] "test"): Blocks: Block 0 [0-52] [Type Unit]: 0 @@ -4111,6 +4127,8 @@ fn partial_eval_stmt_function_calls_from_library() { Package: Entry Expression: 0 Items: + Item 0 [35-35] (Public): + Namespace (Ident 1 [35-35] "test"): Blocks: Block 0 [0-34] [Type Int]: 0 diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs index 8eb91adbb1..2283e805cb 100644 --- a/compiler/qsc_frontend/src/compile.rs +++ b/compiler/qsc_frontend/src/compile.rs @@ -59,6 +59,10 @@ pub struct AstPackage { #[derive(Debug, Default)] pub struct SourceMap { sources: Vec, + /// The common prefix of the sources + /// e.g. if the sources all start with `/Users/microsoft/code/qsharp/src`, then this value is + /// `/Users/microsoft/code/qsharp/src`. + common_prefix: Option>, entry: Option, } @@ -86,8 +90,26 @@ impl SourceMap { offset_sources.push(source); } + // Each source has a name, which is a string. The project root dir is calculated as the + // common prefix of all of the sources. + // Calculate the common prefix. + let common_prefix: String = longest_common_prefix( + &offset_sources + .iter() + .map(|source| source.name.as_ref()) + .collect::>(), + ) + .to_string(); + + let common_prefix: Arc = Arc::from(common_prefix); + Self { sources: offset_sources, + common_prefix: if common_prefix.is_empty() { + None + } else { + Some(common_prefix) + }, entry: entry_source, } } @@ -121,6 +143,25 @@ impl SourceMap { pub fn iter(&self) -> impl Iterator { self.sources.iter() } + + /// Returns the sources as an iter, but with the project root directory subtracted + /// from the individual source names. + pub(crate) fn relative_sources(&self) -> impl Iterator + '_ { + self.sources.iter().map(move |source| { + let name = source.name.as_ref(); + let relative_name = if let Some(common_prefix) = &self.common_prefix { + name.strip_prefix(common_prefix.as_ref()).unwrap_or(name) + } else { + name + }; + + Source { + name: relative_name.into(), + contents: source.contents.clone(), + offset: source.offset, + } + }) + } } #[derive(Clone, Debug)] @@ -421,8 +462,9 @@ fn parse_all( ) -> (ast::Package, Vec) { let mut namespaces = Vec::new(); let mut errors = Vec::new(); - for source in &sources.sources { - let (source_namespaces, source_errors) = qsc_parse::namespaces(&source.contents, features); + for source in sources.relative_sources() { + let (source_namespaces, source_errors) = + qsc_parse::namespaces(&source.contents, Some(&source.name), features); for mut namespace in source_namespaces { Offsetter(source.offset).visit_namespace(&mut namespace); namespaces.push(TopLevelNode::Namespace(namespace)); @@ -530,3 +572,35 @@ fn assert_no_errors(sources: &SourceMap, errors: &mut Vec) { panic!("could not compile package"); } } + +#[must_use] +pub fn longest_common_prefix<'a>(strs: &'a [&'a str]) -> &'a str { + if strs.len() == 1 { + return truncate_to_path_separator(strs[0]); + } + + let Some(common_prefix_so_far) = strs.first() else { + return ""; + }; + + for (i, character) in common_prefix_so_far.chars().enumerate() { + for string in strs { + if string.chars().nth(i) != Some(character) { + let prefix = &common_prefix_so_far[0..i]; + // Find the last occurrence of the path separator in the prefix + return truncate_to_path_separator(prefix); + } + } + } + common_prefix_so_far +} + +fn truncate_to_path_separator(prefix: &str) -> &str { + let last_separator_index = prefix.rfind('/').or_else(|| prefix.rfind('\\')); + if let Some(last_separator_index) = last_separator_index { + // Return the prefix up to and including the last path separator + return &prefix[0..=last_separator_index]; + } + // If there's no path separator in the prefix, return an empty string + "" +} diff --git a/compiler/qsc_frontend/src/compile/tests.rs b/compiler/qsc_frontend/src/compile/tests.rs index 81a3035070..c7227aeacc 100644 --- a/compiler/qsc_frontend/src/compile/tests.rs +++ b/compiler/qsc_frontend/src/compile/tests.rs @@ -3,7 +3,7 @@ #![allow(clippy::needless_raw_string_hashes)] -use super::{compile, CompileUnit, Error, PackageStore, SourceMap}; +use super::{compile, longest_common_prefix, CompileUnit, Error, PackageStore, SourceMap}; use crate::compile::TargetCapabilityFlags; use expect_test::expect; @@ -1291,3 +1291,180 @@ fn hierarchical_namespace_basic() { ); assert!(lib.errors.is_empty(), "{:#?}", lib.errors); } + +#[test] +fn implicit_namespace_basic() { + let sources = SourceMap::new( + [ + ( + "Test.qs".into(), + indoc! {" + operation Bar() : Unit {} + "} + .into(), + ), + ( + "Main.qs".into(), + indoc! {" + @EntryPoint() + operation Bar() : Unit { + Test.Bar(); + open Foo.Bar; + Baz.Quux(); + } + "} + .into(), + ), + ( + "Foo/Bar/Baz.qs".into(), + indoc! {" + operation Quux() : Unit {} + "} + .into(), + ), + ], + None, + ); + let unit = default_compile(sources); + assert!(unit.errors.is_empty(), "{:#?}", unit.errors); +} + +#[test] +fn reject_bad_filename_implicit_namespace() { + let sources = SourceMap::new( + [ + ( + "123Test.qs".into(), + indoc! {" + operation Bar() : Unit {} + "} + .into(), + ), + ( + "Test-File.qs".into(), + indoc! {" + operation Bar() : Unit { + } + "} + .into(), + ), + ( + "Namespace.Foo.qs".into(), + indoc! {" + operation Bar() : Unit {} + "} + .into(), + ), + ], + None, + ); + let unit = default_compile(sources); + expect![[r#" + [ + Error( + Parse( + Error( + InvalidFileName( + Span { + lo: 0, + hi: 25, + }, + "123Test", + ), + ), + ), + ), + Error( + Parse( + Error( + InvalidFileName( + Span { + lo: 27, + hi: 53, + }, + "Test-File", + ), + ), + ), + ), + Error( + Parse( + Error( + InvalidFileName( + Span { + lo: 55, + hi: 80, + }, + "Namespace.Foo", + ), + ), + ), + ), + ] + "#]] + .assert_debug_eq(&unit.errors); +} + +#[test] +fn test_longest_common_prefix_1() { + assert_eq!(longest_common_prefix(&["/a/b/c", "/a/b/d"]), "/a/b/"); +} + +#[test] +fn test_longest_common_prefix_2() { + assert_eq!(longest_common_prefix(&["foo", "bar"]), ""); +} + +#[test] +fn test_longest_common_prefix_3() { + assert_eq!(longest_common_prefix(&["baz", "bar"]), ""); +} + +#[test] +fn test_longest_common_prefix_4() { + assert_eq!(longest_common_prefix(&["baz", "bar"]), ""); +} + +#[test] +fn test_longest_common_prefix_5() { + assert_eq!( + longest_common_prefix(&[ + "code\\project\\src\\Main.qs", + "code\\project\\src\\Helper.qs" + ]), + "code\\project\\src\\" + ); +} + +#[test] +fn test_longest_common_prefix_6() { + assert_eq!( + longest_common_prefix(&["code/project/src/Bar.qs", "code/project/src/Baz.qs"]), + "code/project/src/" + ); +} + +#[test] +fn test_longest_common_prefix_two_relative_paths() { + expect!["a/"].assert_eq(longest_common_prefix(&["a/b", "a/c"])); +} + +#[test] +fn test_longest_common_prefix_one_relative_path() { + expect!["a/"].assert_eq(longest_common_prefix(&["a/b"])); +} + +#[test] +fn test_longest_common_prefix_one_file_name() { + expect![""].assert_eq(longest_common_prefix(&["a"])); +} + +#[test] +fn test_longest_common_prefix_only_root_common() { + expect!["/"].assert_eq(longest_common_prefix(&["/a/b", "/b/c"])); +} + +#[test] +fn test_longest_common_prefix_only_root_common_no_leading() { + expect![""].assert_eq(longest_common_prefix(&["a/b", "b/c"])); +} diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs index 0666dae29d..159284852d 100644 --- a/compiler/qsc_frontend/src/resolve/tests.rs +++ b/compiler/qsc_frontend/src/resolve/tests.rs @@ -147,7 +147,7 @@ fn compile( input: &str, language_features: LanguageFeatures, ) -> (Package, Names, Locals, Vec, NamespaceTreeRoot) { - let (namespaces, parse_errors) = qsc_parse::namespaces(input, language_features); + let (namespaces, parse_errors) = qsc_parse::namespaces(input, None, language_features); assert!(parse_errors.is_empty(), "parse failed: {parse_errors:#?}"); let mut package = Package { id: NodeId::default(), diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs index 9a28600a12..f1dc0c6871 100644 --- a/compiler/qsc_frontend/src/typeck/tests.rs +++ b/compiler/qsc_frontend/src/typeck/tests.rs @@ -105,7 +105,7 @@ fn compile(input: &str, entry_expr: &str) -> (Package, super::Table, Vec Package { - let (namespaces, errors) = qsc_parse::namespaces(input, LanguageFeatures::default()); + let (namespaces, errors) = qsc_parse::namespaces(input, None, LanguageFeatures::default()); assert!(errors.is_empty(), "parsing input failed: {errors:#?}"); let entry = if entry_expr.is_empty() { diff --git a/compiler/qsc_hir/src/hir.rs b/compiler/qsc_hir/src/hir.rs index 7d31aea158..bcff00e40d 100644 --- a/compiler/qsc_hir/src/hir.rs +++ b/compiler/qsc_hir/src/hir.rs @@ -1198,7 +1198,7 @@ impl Idents { self.0.iter() } - /// the conjoined span of all idents in the `VecIdent` + /// the conjoined span of all idents in the `Idents` #[must_use] pub fn span(&self) -> Span { Span { diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs index 7bb742d71c..358ddc594a 100644 --- a/compiler/qsc_parse/src/item.rs +++ b/compiler/qsc_parse/src/item.rs @@ -24,10 +24,11 @@ use crate::{ ErrorKind, }; use qsc_ast::ast::{ - Attr, Block, CallableBody, CallableDecl, CallableKind, Ident, Item, ItemKind, Namespace, - NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, TopLevelNode, Ty, - TyDef, TyDefKind, TyKind, Visibility, VisibilityKind, + Attr, Block, CallableBody, CallableDecl, CallableKind, Ident, Idents, Item, ItemKind, + Namespace, NodeId, Pat, PatKind, Path, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, + TopLevelNode, Ty, TyDef, TyDefKind, TyKind, Visibility, VisibilityKind, }; +use qsc_data_structures::language_features::LanguageFeatures; use qsc_data_structures::span::Span; pub(super) fn parse(s: &mut ParserContext) -> Result> { @@ -131,6 +132,80 @@ fn parse_top_level_node(s: &mut ParserContext) -> Result { Ok(TopLevelNode::Stmt(stmt)) } } +pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Result { + if s.peek().kind == TokenKind::Eof { + return Ok(Namespace { + id: NodeId::default(), + span: s.span(0), + doc: "".into(), + name: source_name_to_namespace_name(source_name, s.span(0))?, + items: Vec::new().into_boxed_slice(), + }); + } + let lo = s.peek().span.lo; + let items = parse_namespace_block_contents(s)?; + if items.is_empty() || s.peek().kind != TokenKind::Eof { + return Err(Error(ErrorKind::ExpectedItem(s.peek().kind, s.span(lo)))); + } + let span = s.span(lo); + let namespace_name = source_name_to_namespace_name(source_name, span)?; + + Ok(Namespace { + id: NodeId::default(), + span, + doc: "".into(), + name: namespace_name, + items: items.into_boxed_slice(), + }) +} + +/// Given a file name, convert it to a namespace name. +/// For example, `foo/bar.qs` becomes `foo.bar`. +fn source_name_to_namespace_name(raw: &str, span: Span) -> Result { + let path = std::path::Path::new(raw); + let mut namespace = Vec::new(); + for component in path.components() { + match component { + std::path::Component::Normal(name) => { + // strip the extension off, if there is one + let mut name = name.to_str().ok_or(Error(ErrorKind::InvalidFileName( + span, + name.to_string_lossy().to_string(), + )))?; + + if let Some(dot) = name.rfind('.') { + name = name[..dot].into(); + } + // verify that the component only contains alphanumeric characters, and doesn't start with a number + + let mut ident = validate_namespace_name(span, name)?; + ident.span = span; + + namespace.push(ident); + } + _ => return Err(Error(ErrorKind::InvalidFileName(span, raw.to_string()))), + } + } + + Ok(namespace.into()) +} + +/// Validates that a string could be a valid namespace name component +fn validate_namespace_name(error_span: Span, name: &str) -> Result { + let mut s = ParserContext::new(name, LanguageFeatures::default()); + // if it could be a valid identifier, then it is a valid namespace name + // we just directly use the ident parser here instead of trying to recreate + // validation rules + let ident = ident(&mut s) + .map_err(|_| Error(ErrorKind::InvalidFileName(error_span, name.to_string())))?; + if s.peek().kind != TokenKind::Eof { + return Err(Error(ErrorKind::InvalidFileName( + error_span, + name.to_string(), + ))); + } + Ok(*ident) +} fn parse_namespace(s: &mut ParserContext) -> Result { let lo = s.peek().span.lo; @@ -138,7 +213,7 @@ fn parse_namespace(s: &mut ParserContext) -> Result { token(s, TokenKind::Keyword(Keyword::Namespace))?; let name = path(s)?; token(s, TokenKind::Open(Delim::Brace))?; - let items = barrier(s, &[TokenKind::Close(Delim::Brace)], parse_many)?; + let items = parse_namespace_block_contents(s)?; recovering_token(s, TokenKind::Close(Delim::Brace)); Ok(Namespace { id: NodeId::default(), @@ -149,6 +224,14 @@ fn parse_namespace(s: &mut ParserContext) -> Result { }) } +/// Parses the contents of a namespace block, what is in between the open and close braces in an +/// explicit namespace, and any top level items in an implicit namespace. +#[allow(clippy::vec_box)] +fn parse_namespace_block_contents(s: &mut ParserContext) -> Result>> { + let items = barrier(s, &[TokenKind::Close(Delim::Brace)], parse_many)?; + Ok(items) +} + /// See [GH Issue 941](https://github.com/microsoft/qsharp/issues/941) for context. /// We want to anticipate docstrings in places people might /// put them, but throw them away. This is to maintain @@ -161,7 +244,7 @@ pub(super) fn throw_away_doc(s: &mut ParserContext) { let _ = parse_doc(s); } -fn parse_doc(s: &mut ParserContext) -> Option { +pub(crate) fn parse_doc(s: &mut ParserContext) -> Option { let mut content = String::new(); while s.peek().kind == TokenKind::DocComment { if !content.is_empty() { diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs index abacbdafa2..40b84a6dd7 100644 --- a/compiler/qsc_parse/src/item/tests.rs +++ b/compiler/qsc_parse/src/item/tests.rs @@ -3,12 +3,13 @@ #![allow(clippy::needless_raw_string_hashes)] -use super::{parse, parse_attr, parse_spec_decl}; +use super::{parse, parse_attr, parse_spec_decl, source_name_to_namespace_name}; use crate::{ scan::ParserContext, tests::{check, check_vec, check_vec_v2_preview}, }; use expect_test::expect; +use qsc_data_structures::span::Span; fn parse_namespaces(s: &mut ParserContext) -> Result, crate::Error> { super::parse_namespaces(s) @@ -41,6 +42,18 @@ fn adjoint_invert() { ); } +// unit tests for file_name_to_namespace_name +#[test] +fn file_name_to_namespace_name() { + let raw = "foo/bar.qs"; + let error_span = Span::default(); + check( + |_| source_name_to_namespace_name(raw, error_span), + "", + &expect![[r#"[Ident _id_ [0-0] "foo", Ident _id_ [0-0] "bar"]"#]], + ); +} + #[test] fn controlled_distribute() { check( diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs index d8fc0c9c54..1b72607a14 100644 --- a/compiler/qsc_parse/src/lib.rs +++ b/compiler/qsc_parse/src/lib.rs @@ -16,15 +16,18 @@ mod stmt; mod tests; mod ty; +use crate::item::parse_doc; +use crate::keyword::Keyword; use lex::TokenKind; use miette::Diagnostic; use qsc_ast::ast::{Expr, Namespace, TopLevelNode}; use qsc_data_structures::{language_features::LanguageFeatures, span::Span}; use scan::ParserContext; +use std::rc::Rc; use std::result; use thiserror::Error; -#[derive(Clone, Copy, Debug, Diagnostic, Eq, Error, PartialEq)] +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] #[error(transparent)] #[diagnostic(transparent)] pub struct Error(ErrorKind); @@ -36,7 +39,7 @@ impl Error { } } -#[derive(Clone, Copy, Debug, Diagnostic, Eq, Error, PartialEq)] +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] enum ErrorKind { #[error(transparent)] #[diagnostic(transparent)] @@ -77,6 +80,12 @@ enum ErrorKind { #[error("dotted namespace aliases are not allowed")] #[diagnostic(code("Qsc.Parse.DotIdentAlias"))] DotIdentAlias(#[label] Span), + #[error("file name {1} could not be converted into valid namespace name")] + #[diagnostic(code("Qsc.Parse.InvalidFileName"))] + InvalidFileName(#[label] Span, String), + #[error("expected an item or EOF, found {0}")] + #[diagnostic(code("Qsc.Parse.ExpectedItem"))] + ExpectedItem(TokenKind, #[label] Span), } impl ErrorKind { @@ -95,6 +104,8 @@ impl ErrorKind { Self::FloatingVisibility(span) => Self::FloatingVisibility(span + offset), Self::MissingSeqEntry(span) => Self::MissingSeqEntry(span + offset), Self::DotIdentAlias(span) => Self::DotIdentAlias(span + offset), + Self::InvalidFileName(span, name) => Self::InvalidFileName(span + offset, name), + Self::ExpectedItem(token, span) => Self::ExpectedItem(token, span + offset), } } } @@ -108,10 +119,38 @@ impl Result> Parser for F {} #[must_use] pub fn namespaces( input: &str, + source_name: Option<&str>, language_features: LanguageFeatures, ) -> (Vec, Vec) { let mut scanner = ParserContext::new(input, language_features); - match item::parse_namespaces(&mut scanner) { + let doc = parse_doc(&mut scanner); + let doc = Rc::from(doc.unwrap_or_default()); + #[allow(clippy::unnecessary_unwrap)] + let result: Result<_> = (|| { + if source_name.is_some() && scanner.peek().kind != TokenKind::Keyword(Keyword::Namespace) { + let mut ns = item::parse_implicit_namespace( + source_name.expect("invariant checked above via `.is_some()`"), + &mut scanner, + ) + .map(|x| vec![x])?; + if let Some(ref mut ns) = ns.get_mut(0) { + if let Some(x) = ns.items.get_mut(0) { + x.span.lo = 0; + x.doc = doc; + }; + } + Ok(ns) + } else { + let mut ns = item::parse_namespaces(&mut scanner)?; + if let Some(x) = ns.get_mut(0) { + x.span.lo = 0; + x.doc = doc; + }; + Ok(ns) + } + })(); + + match result { Ok(namespaces) => (namespaces, scanner.into_errors()), Err(error) => { let mut errors = scanner.into_errors(); diff --git a/compiler/qsc_parse/src/tests.rs b/compiler/qsc_parse/src/tests.rs index c9f5ee8e3e..bc77344874 100644 --- a/compiler/qsc_parse/src/tests.rs +++ b/compiler/qsc_parse/src/tests.rs @@ -7,6 +7,8 @@ use expect_test::Expect; use qsc_data_structures::language_features::LanguageFeatures; use std::fmt::Display; +mod implicit_namespace; + pub(super) fn check(parser: impl Parser, input: &str, expect: &Expect) { check_map(parser, input, expect, ToString::to_string); } diff --git a/compiler/qsc_parse/src/tests/implicit_namespace.rs b/compiler/qsc_parse/src/tests/implicit_namespace.rs new file mode 100644 index 0000000000..ce9630452b --- /dev/null +++ b/compiler/qsc_parse/src/tests/implicit_namespace.rs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use expect_test::expect; +use qsc_data_structures::language_features::LanguageFeatures; + +#[test] +fn explicit_namespace_overrides_implicit() { + let result = format!( + "{:#?}", + crate::namespaces( + "namespace Explicit {}", + Some("code/src/Implicit.qs"), + LanguageFeatures::default() + ) + ); + expect![[r#" + ( + [ + Namespace { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 0, + hi: 21, + }, + doc: "", + name: Idents( + [ + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 10, + hi: 18, + }, + name: "Explicit", + }, + ], + ), + items: [], + }, + ], + [], + )"#]] + .assert_eq(&result); +} + +#[test] +fn reject_bad_namespace_name_1() { + let result = format!( + "{:#?}", + crate::namespaces( + "operation Main() : Unit {}", + Some("code/src/Foo-Bar.qs"), + LanguageFeatures::default() + ) + ); + expect![[r#" + ( + [], + [ + Error( + InvalidFileName( + Span { + lo: 0, + hi: 26, + }, + "Foo-Bar", + ), + ), + ], + )"#]] + .assert_eq(&result); +} + +#[test] +fn reject_bad_namespace_name_2() { + let result = format!( + "{:#?}", + crate::namespaces( + "operation Main() : Unit {}", + Some("code/src/123Bar.qs"), + LanguageFeatures::default() + ) + ); + expect![[r#" + ( + [], + [ + Error( + InvalidFileName( + Span { + lo: 0, + hi: 26, + }, + "123Bar", + ), + ), + ], + )"#]] + .assert_eq(&result); +} diff --git a/compiler/qsc_passes/src/spec_gen/tests.rs b/compiler/qsc_passes/src/spec_gen/tests.rs index a791a8da2f..9bf8bba0e1 100644 --- a/compiler/qsc_passes/src/spec_gen/tests.rs +++ b/compiler/qsc_passes/src/spec_gen/tests.rs @@ -1968,7 +1968,7 @@ fn op_array_forget_functors_with_lambdas() { ", &expect![[r#" Package: - Item 0 [13-328] (Public): + Item 0 [0-328] (Public): Namespace (Ident 52 [23-24] "A"): Item 1, Item 2, Item 3, Item 4 Item 1 [43-75] (Public): Parent: 0 diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 5ce88601f7..ecc93fd48c 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -149,6 +149,9 @@ async fn close_last_doc_in_project() { offset: 59, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -164,14 +167,13 @@ async fn close_last_doc_in_project() { Error( Parse( Error( - Token( - Eof, + ExpectedItem( ClosedBinOp( Slash, ), Span { lo: 59, - hi: 60, + hi: 59, }, ), ), @@ -286,12 +288,11 @@ async fn compile_error() { Error( Parse( Error( - Token( - Eof, + ExpectedItem( Ident, Span { lo: 0, - hi: 9, + hi: 0, }, ), ), @@ -909,6 +910,9 @@ async fn update_doc_updates_project() { offset: 59, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -999,6 +1003,9 @@ async fn close_doc_prioritizes_fs() { offset: 59, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -1014,14 +1021,13 @@ async fn close_doc_prioritizes_fs() { Error( Parse( Error( - Token( - Eof, + ExpectedItem( ClosedBinOp( Slash, ), Span { lo: 59, - hi: 60, + hi: 59, }, ), ), @@ -1078,6 +1084,9 @@ async fn delete_manifest() { offset: 71, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -1113,6 +1122,9 @@ async fn delete_manifest() { offset: 0, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -1157,6 +1169,9 @@ async fn delete_manifest_then_close() { offset: 71, }, ], + common_prefix: Some( + "project/src/", + ), entry: None, } "#]], @@ -1218,6 +1233,9 @@ async fn doc_switches_project() { offset: 15, }, ], + common_prefix: Some( + "nested_projects/src/subdir/src/", + ), entry: None, } "#]], @@ -1268,6 +1286,9 @@ async fn doc_switches_project() { offset: 15, }, ], + common_prefix: Some( + "nested_projects/src/subdir/src/", + ), entry: None, } "#]], @@ -1317,6 +1338,9 @@ async fn doc_switches_project_on_close() { offset: 15, }, ], + common_prefix: Some( + "nested_projects/src/subdir/src/", + ), entry: None, } "#]], @@ -1360,6 +1384,9 @@ async fn doc_switches_project_on_close() { offset: 15, }, ], + common_prefix: Some( + "nested_projects/src/subdir/src/", + ), entry: None, } "#]], diff --git a/language_service/src/tests.rs b/language_service/src/tests.rs index 4b5bf84f5f..c89443db32 100644 --- a/language_service/src/tests.rs +++ b/language_service/src/tests.rs @@ -52,6 +52,7 @@ async fn single_document() { offset: 0, }, ], + common_prefix: None, entry: None, } "#]]), @@ -99,6 +100,7 @@ async fn single_document_update() { offset: 0, }, ], + common_prefix: None, entry: None, } "#]]), @@ -137,6 +139,7 @@ async fn single_document_update() { offset: 0, }, ], + common_prefix: None, entry: None, } "#]]), @@ -197,6 +200,7 @@ async fn document_in_project() { offset: 52, }, ], + common_prefix: None, entry: None, } "#]],