Recognize unqualified import syntax in double representation library. (#3816)

Fixes [#183511669](https://www.pivotaltracker.com/story/show/183511669)

This PR improves the `ImportInfo` structure from Double Representation crate so it provides information what names are brought into the scope with this import. The from_ast method also recognized [various ways of defining imports](https://github.com/enso-org/enso/blob/develop/docs/syntax/imports.md)

# Important Notes
The parser seems to not recognize properly imports with aliases. I have not fixed that, as we expect a new parser to be merged very soon.
This commit is contained in:
Adam Obuchowicz 2022-10-27 15:22:06 +02:00 committed by GitHub
parent 3a8aa90f1b
commit c4a7e28fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 375 additions and 129 deletions

View File

@ -0,0 +1,307 @@
//! A module with utilities managing imports.
use crate::prelude::*;
use crate::module;
use ast::known;
use ast::Ast;
use ast::HasRepr;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;
// =================
// === Constants ===
// =================
const LIST_SEPARATOR: char = ',';
const ALIAS_KEYWORD: &str = "as";
const ALL_KEYWORD: &str = "all";
const HIDING_KEYWORD: &str = "hiding";
// ===============
// === Aliases ===
// ===============
/// Id for an import.
pub type Id = u64;
// =====================
// === ImportedNames ===
// =====================
/// A structure describing what names are imported from the module in a specific import declaration.
#[allow(missing_docs)]
#[derive(Clone, Debug, Eq, Deserialize, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum ImportedNames {
/// The import is `import <module> [as <alias>]` and only module name is imported.
Module { alias: Option<String> },
/// The import is `from <module> import all`, and all names defined in the module are imported.
All,
/// The import is `from <module> import all hiding <not_imported>`, and all names except
/// specified in `not_imported` list are imported
AllExcept { not_imported: BTreeSet<String> },
/// The import is `from <module> import <names>`, and only the specified `names` are imported.
List { names: BTreeSet<String> },
}
impl ImportedNames {
/// Create [`ImportedNames`] structure from the second `Match` segment body.
///
/// The unqualified imports are always parsed as [`Match`](crate::Shape::Match) AST node, where
/// the second segment starts from `import` and ends with end of the import declaration. Thus,
/// the second segment body may be `all`, `all hiding <comma-separated-name-list>`, or just
/// comma separated name list.
fn from_unqualified_import_match_second_segment(segment: impl AsRef<str>) -> Self {
let is_token_sep = |c: char| c.is_ascii_whitespace() || c == LIST_SEPARATOR;
let scope_split = segment.as_ref().split(is_token_sep);
let mut scope_tokens = scope_split.filter(|tok| !tok.is_empty());
let first_token = scope_tokens.next();
let second_token = scope_tokens.next();
let third_and_further_tokens = scope_tokens;
match (first_token, second_token) {
(Some("all"), Some("hiding")) =>
Self::AllExcept { not_imported: third_and_further_tokens.map(Into::into).collect() },
(Some("all"), _) => Self::All,
(first_name, second_name) => {
let all_names =
first_name.into_iter().chain(second_name).chain(third_and_further_tokens);
Self::List { names: all_names.map(Into::into).collect() }
}
}
}
}
/// Representation of a single import declaration.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize, Hash)]
pub struct Info {
/// The segments of the qualified name of the imported module.
///
/// This field is not Qualified name to cover semantically illegal imports that are possible to
/// be typed in and are representable in the text.
/// This includes targets with too few segments or segments not being valid referent names.
pub module: Vec<String>,
/// Imported names from [`module`].
pub imported: ImportedNames,
}
impl Info {
/// Create qualified import (i.e. `import <module-name>`) importing the given module without
/// alias.
pub fn new_qualified(module: &module::QualifiedName) -> Self {
Self {
module: module.segments().map(|segment| segment.to_string()).collect(),
imported: ImportedNames::Module { alias: None },
}
}
/// Obtain the qualified name of the module.
pub fn qualified_module_name(&self) -> FallibleResult<module::QualifiedName> {
module::QualifiedName::from_all_segments(&self.module)
}
/// Construct from an AST. Fails if the Ast is not an import declaration.
pub fn from_ast(ast: &Ast) -> Option<Self> {
let macro_match = known::Match::try_from(ast).ok()?;
Self::from_match(macro_match)
}
/// Construct from a macro match AST. Fails if the Ast is not an import declaration.
pub fn from_match(ast: known::Match) -> Option<Self> {
if ast::macros::is_match_qualified_import(&ast) {
Some(Self {
module: Self::module_name_from_str(ast.segs.head.body.repr()),
// TODO[ao] the current parser does not recognize aliases for imports. Should be
// fixed with the new parser. Once new parser will be integrated, the alias
// support will be implemented as task
// https://www.pivotaltracker.com/story/show/183590537
imported: ImportedNames::Module { alias: None },
})
} else if ast::macros::is_match_unqualified_import(&ast) {
let module = ast.segs.head.body.repr();
let imported = ast.segs.tail.first().map_or_default(|s| s.body.repr());
Some(Self::from_module_and_scope_str(module, imported))
} else {
None
}
}
/// Create [`Info`] from unqualified import segment's body representations.
///
/// The unqualified imports are always parsed as [`Match`](crate::Shape::Match) AST node, where
/// the first segment contains keyword `from` and module name, and second segment the rest of
/// the import.
fn from_module_and_scope_str(module: impl AsRef<str>, imported: impl AsRef<str>) -> Self {
Self {
module: Self::module_name_from_str(module),
imported: ImportedNames::from_unqualified_import_match_second_segment(imported),
}
}
fn module_name_from_str(module: impl AsRef<str>) -> Vec<String> {
let name = module.as_ref().trim();
if name.is_empty() {
Vec::new()
} else {
let segments = name.split(ast::opr::predefined::ACCESS);
let trimmed = segments.map(str::trim);
trimmed.map(Into::into).collect()
}
}
/// Return the ID of the import.
///
/// The ID is based on a hash of the qualified name of the imported target. This ID is GUI
/// internal and not known in the engine.
pub fn id(&self) -> Id {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
impl Display for Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let module = self.module.join(ast::opr::predefined::ACCESS);
let import_kw = ast::macros::QUALIFIED_IMPORT_KEYWORD;
let from_kw = ast::macros::UNQUALIFIED_IMPORT_KEYWORD;
match &self.imported {
ImportedNames::Module { alias } => {
write!(f, "{} {}", import_kw, module)?;
if let Some(alias) = alias {
write!(f, " {} {}", ALIAS_KEYWORD, alias)?;
}
Ok(())
}
ImportedNames::All => write!(f, "{} {} {} {}", from_kw, module, import_kw, ALL_KEYWORD),
ImportedNames::List { names } => {
let names = names.iter().join(", ");
write!(f, "{} {} {} {}", from_kw, module, import_kw, names)
}
ImportedNames::AllExcept { not_imported: hidden_names } => {
let names = hidden_names.iter().join(", ");
write!(
f,
"{} {} {} {} {} {}",
from_kw, module, import_kw, ALL_KEYWORD, HIDING_KEYWORD, names
)
}
}
}
}
// =============
// === Tests ===
// =============
#[cfg(test)]
mod tests {
use super::*;
use parser_scala::Parser;
struct Fixture {
parser: Parser,
}
impl Fixture {
fn new() -> Self {
Self { parser: Parser::new_or_panic() }
}
fn run_case(&self, code: &str, expected: Info) {
let ast = self.parser.parse_line_ast(code).expect("Parsing import declaration failed");
let info = Info::from_ast(&ast);
assert_eq!(info, Some(expected));
}
}
#[wasm_bindgen_test]
fn qualified_import_info_from_ast() {
let test = Fixture::new();
let make_info = |module: &[&str]| Info {
module: module.iter().map(|&s| s.to_owned()).collect(),
imported: ImportedNames::Module { alias: None },
};
let normal_case = "import Standard.Base.Data";
let normal_case_expected = make_info(&["Standard", "Base", "Data"]);
test.run_case(normal_case, normal_case_expected);
let weird_spaces = "import Standard .Base . Data ";
let weird_spaces_expected = make_info(&["Standard", "Base", "Data"]);
test.run_case(weird_spaces, weird_spaces_expected);
let single_segment = "import local";
let single_segment_expected = make_info(&["local"]);
test.run_case(single_segment, single_segment_expected);
}
#[wasm_bindgen_test]
fn unrestricted_import_info_from_ast() {
let test = Fixture::new();
let make_info = |module: &[&str]| Info {
module: module.iter().map(|&s| s.to_owned()).collect(),
imported: ImportedNames::All,
};
let normal_case = "from Standard.Base import all";
let normal_case_expected = make_info(&["Standard", "Base"]);
test.run_case(normal_case, normal_case_expected);
let weird_spaces = "from Standard . Base import all ";
let weird_spaces_expected = make_info(&["Standard", "Base"]);
test.run_case(weird_spaces, weird_spaces_expected);
}
#[wasm_bindgen_test]
fn restricted_import_info_from_ast() {
let test = Fixture::new();
let make_info = |module: &[&str], names: &[&str]| Info {
module: module.iter().map(|&s| s.to_owned()).collect(),
imported: ImportedNames::List { names: names.iter().map(|&s| s.to_owned()).collect() },
};
let normal_case = "from Standard.Base import Foo, Bar";
let normal_case_expected = make_info(&["Standard", "Base"], &["Foo", "Bar"]);
test.run_case(normal_case, normal_case_expected);
let weird_spaces = "from Standard . Base import Foo , Bar ,Buz";
let weird_spaces_expected = make_info(&["Standard", "Base"], &["Foo", "Bar", "Buz"]);
test.run_case(weird_spaces, weird_spaces_expected);
let single_name = "from Standard.Base import Foo";
let single_name_expected = make_info(&["Standard", "Base"], &["Foo"]);
test.run_case(single_name, single_name_expected);
}
#[wasm_bindgen_test]
fn hiding_import_info_from_ast() {
let test = Fixture::new();
let make_info = |module: &[&str], hidden_names: &[&str]| Info {
module: module.iter().map(|&s| s.to_owned()).collect(),
imported: ImportedNames::AllExcept {
not_imported: hidden_names.iter().map(|&s| s.to_owned()).collect(),
},
};
let normal_case = "from Standard.Base import all hiding Foo, Bar";
let normal_case_expected = make_info(&["Standard", "Base"], &["Foo", "Bar"]);
test.run_case(normal_case, normal_case_expected);
let weird_spaces = "from Standard . Base import all hiding Foo , Bar ,Buz";
let weird_spaces_expected = make_info(&["Standard", "Base"], &["Foo", "Bar", "Buz"]);
test.run_case(weird_spaces, weird_spaces_expected);
let single_name = "from Standard.Base import all hiding Foo";
let single_name_expected = make_info(&["Standard", "Base"], &["Foo"]);
test.run_case(single_name, single_name_expected);
}
}

View File

@ -6,6 +6,7 @@
#![feature(iter_order_by)] #![feature(iter_order_by)]
#![feature(option_result_contains)] #![feature(option_result_contains)]
#![feature(type_alias_impl_trait)] #![feature(type_alias_impl_trait)]
#![feature(iter_next_chunk)]
// === Standard Linter Configuration === // === Standard Linter Configuration ===
#![deny(non_ascii_idents)] #![deny(non_ascii_idents)]
#![warn(unsafe_code)] #![warn(unsafe_code)]
@ -43,6 +44,7 @@ pub mod connection;
pub mod definition; pub mod definition;
pub mod graph; pub mod graph;
pub mod identifier; pub mod identifier;
pub mod import;
pub mod module; pub mod module;
pub mod node; pub mod node;
pub mod project; pub mod project;

View File

@ -10,6 +10,7 @@ use crate::identifier;
use crate::identifier::Identifier; use crate::identifier::Identifier;
use crate::identifier::LocatedName; use crate::identifier::LocatedName;
use crate::identifier::ReferentName; use crate::identifier::ReferentName;
use crate::import;
use crate::project; use crate::project;
use crate::tp; use crate::tp;
@ -31,12 +32,12 @@ use serde::Serialize;
#[derive(Clone, Debug, Fail)] #[derive(Clone, Debug, Fail)]
#[fail(display = "Import `{}` was not found in the module.", _0)] #[fail(display = "Import `{}` was not found in the module.", _0)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct ImportNotFound(pub ImportInfo); pub struct ImportNotFound(pub import::Info);
#[derive(Clone, Copy, Debug, Fail)] #[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Import with ID `{}` was not found in the module.", _0)] #[fail(display = "Import with ID `{}` was not found in the module.", _0)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct ImportIdNotFound(pub ImportId); pub struct ImportIdNotFound(pub import::Id);
#[derive(Clone, Copy, Debug, Fail)] #[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Line index is out of bounds.")] #[fail(display = "Line index is out of bounds.")]
@ -406,83 +407,6 @@ impl PartialEq<tp::QualifiedName> for QualifiedName {
// ==================
// === ImportInfo ===
// ==================
/// Id for an import.
pub type ImportId = u64;
/// Representation of a single import declaration.
// TODO [mwu]
// Currently only supports the unqualified imports like `import Foo.Bar`. Qualified, restricted and
// and hiding imports are not supported by the parser yet. In future when parser and engine
// supports them, this structure should be adjusted as well.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, Hash)]
pub struct ImportInfo {
/// The segments of the qualified name of the imported target.
///
/// This field is not Qualified name to cover semantically illegal imports that are possible to
/// be typed in and are representable in the text.
/// This includes targets with too few segments or segments not being valid referent names.
pub target: Vec<String>,
}
impl ImportInfo {
/// Construct from a string describing an import target, like `"Foo.Bar"`.
pub fn from_target_str(name: impl AsRef<str>) -> Self {
let name = name.as_ref().trim();
let target = if name.is_empty() {
Vec::new()
} else {
name.split(ast::opr::predefined::ACCESS).map(Into::into).collect()
};
ImportInfo { target }
}
/// Construct from a module qualified name like `"Foo.Bar"` that describes imported target.
pub fn from_qualified_name(name: &QualifiedName) -> Self {
let target = name.segments().map(|segment| segment.to_string()).collect();
Self { target }
}
/// Obtain the qualified name of the imported module.
pub fn qualified_name(&self) -> FallibleResult<QualifiedName> {
QualifiedName::from_all_segments(&self.target)
}
/// Construct from an AST. Fails if the Ast is not an import declaration.
pub fn from_ast(ast: &Ast) -> Option<Self> {
let macro_match = known::Match::try_from(ast).ok()?;
Self::from_match(macro_match)
}
/// Construct from a macro match AST. Fails if the Ast is not an import declaration.
pub fn from_match(ast: known::Match) -> Option<Self> {
ast::macros::is_match_import(&ast)
.then(|| ImportInfo::from_target_str(ast.segs.head.body.repr().trim()))
}
/// Return the ID of the import.
///
/// The ID is based on a hash of the qualified name of the imported target. This ID is GUI
/// internal and not known in the engine.
pub fn id(&self) -> ImportId {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
impl Display for ImportInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let target = self.target.join(ast::opr::predefined::ACCESS);
write!(f, "{} {}", ast::macros::QUALIFIED_IMPORT_KEYWORD, target)
}
}
// ============ // ============
// === Info === // === Info ===
// ============ // ============
@ -514,15 +438,15 @@ impl Info {
} }
/// Iterate over all lines in module that contain an import declaration. /// Iterate over all lines in module that contain an import declaration.
pub fn enumerate_imports(&self) -> impl Iterator<Item = (ModuleCrumb, ImportInfo)> + '_ { pub fn enumerate_imports(&self) -> impl Iterator<Item = (ModuleCrumb, import::Info)> + '_ {
let children = self.ast.shape().enumerate(); let children = self.ast.shape().enumerate();
children.filter_map(|(crumb, ast)| Some((crumb, ImportInfo::from_ast(ast)?))) children.filter_map(|(crumb, ast)| Some((crumb, import::Info::from_ast(ast)?)))
} }
/// Iterate over all import declarations in the module. /// Iterate over all import declarations in the module.
/// ///
/// If the caller wants to know *where* the declarations are, use `enumerate_imports`. /// If the caller wants to know *where* the declarations are, use `enumerate_imports`.
pub fn iter_imports(&self) -> impl Iterator<Item = ImportInfo> + '_ { pub fn iter_imports(&self) -> impl Iterator<Item = import::Info> + '_ {
self.enumerate_imports().map(|(_, import)| import) self.enumerate_imports().map(|(_, import)| import)
} }
@ -548,7 +472,7 @@ impl Info {
/// ///
/// If there is more than one line matching, only the first one will be removed. /// If there is more than one line matching, only the first one will be removed.
/// Fails if there is no import matching given argument. /// Fails if there is no import matching given argument.
pub fn remove_import(&mut self, to_remove: &ImportInfo) -> FallibleResult { pub fn remove_import(&mut self, to_remove: &import::Info) -> FallibleResult {
let lookup_result = self.enumerate_imports().find(|(_, import)| import == to_remove); let lookup_result = self.enumerate_imports().find(|(_, import)| import == to_remove);
let (crumb, _) = lookup_result.ok_or_else(|| ImportNotFound(to_remove.clone()))?; let (crumb, _) = lookup_result.ok_or_else(|| ImportNotFound(to_remove.clone()))?;
self.remove_line(crumb.line_index)?; self.remove_line(crumb.line_index)?;
@ -559,7 +483,7 @@ impl Info {
/// ///
/// If there is more than one line matching, only the first one will be removed. /// If there is more than one line matching, only the first one will be removed.
/// Fails if there is no import matching given argument. /// Fails if there is no import matching given argument.
pub fn remove_import_by_id(&mut self, to_remove: ImportId) -> FallibleResult { pub fn remove_import_by_id(&mut self, to_remove: import::Id) -> FallibleResult {
let lookup_result = self.enumerate_imports().find(|(_, import)| import.id() == to_remove); let lookup_result = self.enumerate_imports().find(|(_, import)| import.id() == to_remove);
let (crumb, _) = lookup_result.ok_or(ImportIdNotFound(to_remove))?; let (crumb, _) = lookup_result.ok_or(ImportIdNotFound(to_remove))?;
self.remove_line(crumb.line_index)?; self.remove_line(crumb.line_index)?;
@ -573,10 +497,10 @@ impl Info {
// TODO [mwu] // TODO [mwu]
// Ideally we should not require parser but should use some sane way of generating AST from // Ideally we should not require parser but should use some sane way of generating AST from
// the `ImportInfo` value. // the `ImportInfo` value.
pub fn add_import(&mut self, parser: &parser_scala::Parser, to_add: ImportInfo) -> usize { pub fn add_import(&mut self, parser: &parser_scala::Parser, to_add: import::Info) -> usize {
// Find last import that is not "after" the added one lexicographically. // Find last import that is not "after" the added one lexicographically.
let previous_import = let previous_import =
self.enumerate_imports().take_while(|(_, import)| to_add.target > import.target).last(); self.enumerate_imports().take_while(|(_, import)| &to_add > import).last();
let index_to_place_at = previous_import.map_or(0, |(crumb, _)| crumb.line_index + 1); let index_to_place_at = previous_import.map_or(0, |(crumb, _)| crumb.line_index + 1);
let import_ast = parser.parse_line_ast(to_add.to_string()).unwrap(); let import_ast = parser.parse_line_ast(to_add.to_string()).unwrap();
@ -592,7 +516,7 @@ impl Info {
to_add: &QualifiedName, to_add: &QualifiedName,
) { ) {
let is_here = to_add == here; let is_here = to_add == here;
let import = ImportInfo::from_qualified_name(to_add); let import = import::Info::new_qualified(to_add);
let already_imported = self.iter_imports().any(|imp| imp == import); let already_imported = self.iter_imports().any(|imp| imp == import);
if !is_here && !already_imported { if !is_here && !already_imported {
self.add_import(parser, import); self.add_import(parser, import);
@ -885,7 +809,7 @@ mod tests {
let imports = info.iter_imports().collect_vec(); let imports = info.iter_imports().collect_vec();
assert_eq!(imports.len(), expected.len()); assert_eq!(imports.len(), expected.len());
for (import, expected_segments) in imports.iter().zip(expected) { for (import, expected_segments) in imports.iter().zip(expected) {
itertools::assert_equal(import.target.iter(), expected_segments.iter()); itertools::assert_equal(import.module.iter(), expected_segments.iter());
} }
}; };
@ -907,7 +831,7 @@ mod tests {
let mut info = Info { ast }; let mut info = Info { ast };
let import = |code| { let import = |code| {
let ast = parser.parse_line_ast(code).unwrap(); let ast = parser.parse_line_ast(code).unwrap();
ImportInfo::from_ast(&ast).unwrap() import::Info::from_ast(&ast).unwrap()
}; };
info.add_import(&parser, import("import Bar.Gar")); info.add_import(&parser, import("import Bar.Gar"));
@ -915,13 +839,13 @@ mod tests {
info.add_import(&parser, import("import Gar.Bar")); info.add_import(&parser, import("import Gar.Bar"));
info.expect_code("import Bar.Gar\nimport Foo.Bar.Baz\nimport Gar.Bar"); info.expect_code("import Bar.Gar\nimport Foo.Bar.Baz\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Foo.Bar.Baz")).unwrap(); info.remove_import(&import("import Foo.Bar.Baz")).unwrap();
info.expect_code("import Bar.Gar\nimport Gar.Bar"); info.expect_code("import Bar.Gar\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Foo.Bar.Baz")).unwrap_err(); info.remove_import(&import("import Foo.Bar.Baz")).unwrap_err();
info.expect_code("import Bar.Gar\nimport Gar.Bar"); info.expect_code("import Bar.Gar\nimport Gar.Bar");
info.remove_import(&ImportInfo::from_target_str("Gar.Bar")).unwrap(); info.remove_import(&import("import Gar.Bar")).unwrap();
info.expect_code("import Bar.Gar"); info.expect_code("import Bar.Gar");
info.remove_import(&ImportInfo::from_target_str("Bar.Gar")).unwrap(); info.remove_import(&import("import Bar.Gar")).unwrap();
info.expect_code(""); info.expect_code("");
info.add_import(&parser, import("import Bar.Gar")); info.add_import(&parser, import("import Bar.Gar"));

View File

@ -1113,7 +1113,7 @@ pub trait HasRepr {
/// Check if the representation is empty. /// Check if the representation is empty.
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.len() >= 0.bytes() self.len() <= 0.bytes()
} }
/// Get the representation length in chars. /// Get the representation length in chars.

View File

@ -207,25 +207,32 @@ pub fn ast_as_import_match(ast: &Ast) -> Option<known::Match> {
is_match_import(&macro_match).then_some(macro_match) is_match_import(&macro_match).then_some(macro_match)
} }
/// Check if the given macro match node is an import declaration. /// If the given AST node is a qualified import declaration (`import <module name>`), returns it as
pub fn is_match_import(ast: &known::Match) -> bool { /// a Match (which is the only shape capable of storing import declarations). Returns `None`
/// otherwise.
pub fn is_match_qualified_import(ast: &known::Match) -> bool {
let segment = &ast.segs.head; let segment = &ast.segs.head;
let keyword = crate::identifier::name(&segment.head); let keyword = crate::identifier::name(&segment.head);
if keyword.contains_if(|str| *str == UNQUALIFIED_IMPORT_KEYWORD) {
let second_segment = &ast.segs.tail.first();
match second_segment {
Some(seg) => {
let keyword_2 = crate::identifier::name(&seg.head);
if keyword_2.contains_if(|str| *str == QUALIFIED_IMPORT_KEYWORD) {
return true;
}
}
None => return false,
}
}
keyword.contains_if(|str| *str == QUALIFIED_IMPORT_KEYWORD) keyword.contains_if(|str| *str == QUALIFIED_IMPORT_KEYWORD)
} }
/// If the given AST node is an unqualified import declaration (`from <module name> import <...>`),
/// returns it as a Match (which is the only shape capable of storing import declarations). Returns
/// `None` otherwise.
pub fn is_match_unqualified_import(ast: &known::Match) -> bool {
let first_segment = &ast.segs.head;
let first_keyword = crate::identifier::name(&first_segment.head);
let second_segment = &ast.segs.tail.first();
let second_keyword = second_segment.and_then(|s| crate::identifier::name(&s.head));
first_keyword == Some(UNQUALIFIED_IMPORT_KEYWORD)
&& second_keyword == Some(QUALIFIED_IMPORT_KEYWORD)
}
/// Check if the given macro match node is an import declaration.
pub fn is_match_import(ast: &known::Match) -> bool {
is_match_qualified_import(ast) || is_match_unqualified_import(ast)
}
/// Check if the given ast node is an import declaration. /// Check if the given ast node is an import declaration.
pub fn is_ast_import(ast: &Ast) -> bool { pub fn is_ast_import(ast: &Ast) -> bool {
ast_as_import_match(ast).is_some() ast_as_import_match(ast).is_some()

View File

@ -412,7 +412,9 @@ impl Fixture {
]; ];
for macro_usage in macro_usages.iter() { for macro_usage in macro_usages.iter() {
println!(">>>>>>>>>> {}", macro_usage);
let ast = self.parser.parse_line_ast(*macro_usage).unwrap(); let ast = self.parser.parse_line_ast(*macro_usage).unwrap();
println!("{:?}", ast);
expect_shape::<Match<Ast>>(&ast); expect_shape::<Match<Ast>>(&ast);
} }
} }

View File

@ -7,6 +7,7 @@ use crate::model::module::TextChange;
use ast; use ast;
use ast::HasIdMap; use ast::HasIdMap;
use double_representation::import;
use double_representation::module; use double_representation::module;
use double_representation::project; use double_representation::project;
use double_representation::text::apply_code_change_to_id_map; use double_representation::text::apply_code_change_to_id_map;
@ -154,7 +155,7 @@ impl Handle {
/// ///
/// May create duplicate entries if such import was already present. /// May create duplicate entries if such import was already present.
pub fn add_import(&self, target: &module::QualifiedName) -> FallibleResult { pub fn add_import(&self, target: &module::QualifiedName) -> FallibleResult {
let import = module::ImportInfo::from_qualified_name(target); let import = import::Info::new_qualified(target);
self.modify(|info| info.add_import(&self.parser, import))?; self.modify(|info| info.add_import(&self.parser, import))?;
Ok(()) Ok(())
} }
@ -163,12 +164,12 @@ impl Handle {
/// ///
/// Fails, if there was no such declaration found. /// Fails, if there was no such declaration found.
pub fn remove_import(&self, target: &module::QualifiedName) -> FallibleResult { pub fn remove_import(&self, target: &module::QualifiedName) -> FallibleResult {
let import = module::ImportInfo::from_qualified_name(target); let import = import::Info::new_qualified(target);
self.modify(|info| info.remove_import(&import))? self.modify(|info| info.remove_import(&import))?
} }
/// Retrieve a vector describing all import declarations currently present in the module. /// Retrieve a vector describing all import declarations currently present in the module.
pub fn imports(&self) -> Vec<module::ImportInfo> { pub fn imports(&self) -> Vec<import::Info> {
let module = self.module_info(); let module = self.module_info();
module.iter_imports().collect() module.iter_imports().collect()
} }

View File

@ -16,7 +16,7 @@ use breadcrumbs::Breadcrumbs;
use const_format::concatcp; use const_format::concatcp;
use double_representation::graph::GraphInfo; use double_representation::graph::GraphInfo;
use double_representation::graph::LocationHint; use double_representation::graph::LocationHint;
use double_representation::module::ImportInfo; use double_representation::import;
use double_representation::module::QualifiedName; use double_representation::module::QualifiedName;
use double_representation::node::NodeInfo; use double_representation::node::NodeInfo;
use double_representation::project; use double_representation::project;
@ -1058,7 +1058,7 @@ impl Searcher {
let without_enso_project = imports.filter(|i| i.to_string() != ENSO_PROJECT_SPECIAL_MODULE); let without_enso_project = imports.filter(|i| i.to_string() != ENSO_PROJECT_SPECIAL_MODULE);
for mut import in without_enso_project { for mut import in without_enso_project {
import.remove_main_module_segment(); import.remove_main_module_segment();
let import_info = ImportInfo::from_qualified_name(&import); let import_info = import::Info::new_qualified(&import);
let already_exists = module.iter_imports().contains(&import_info); let already_exists = module.iter_imports().contains(&import_info);
if already_exists { if already_exists {
@ -1319,7 +1319,10 @@ impl Searcher {
Some(module_name) Some(module_name)
} else { } else {
self.module().iter_imports().find_map(|import| { self.module().iter_imports().find_map(|import| {
import.qualified_name().ok().filter(|module| module.name().deref() == this_name) import
.qualified_module_name()
.ok()
.filter(|module| module.name().deref() == this_name)
}) })
} }
}) })
@ -2421,7 +2424,7 @@ pub mod test {
let module_info = module.info(); let module_info = module.info();
let imported_names = module_info let imported_names = module_info
.iter_imports() .iter_imports()
.map(|import| import.qualified_name().unwrap()) .map(|import| import.qualified_module_name().unwrap())
.collect_vec(); .collect_vec();
let expected_import = expected_import.into_iter().cloned().collect_vec(); let expected_import = expected_import.into_iter().cloned().collect_vec();

View File

@ -8,7 +8,7 @@ use ast::constants::LANGUAGE_FILE_EXTENSION;
use ast::constants::SOURCE_DIRECTORY; use ast::constants::SOURCE_DIRECTORY;
use double_representation::definition::DefinitionInfo; use double_representation::definition::DefinitionInfo;
use double_representation::identifier::ReferentName; use double_representation::identifier::ReferentName;
use double_representation::module::ImportId; use double_representation::import;
use double_representation::project; use double_representation::project;
use engine_protocol::language_server::MethodPointer; use engine_protocol::language_server::MethodPointer;
use flo_stream::Subscriber; use flo_stream::Subscriber;
@ -44,7 +44,7 @@ pub struct NodeMetadataNotFound(pub ast::Id);
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Fail)] #[derive(Debug, Clone, Copy, Fail)]
#[fail(display = "Import with ID {} was not found in metadata.", _0)] #[fail(display = "Import with ID {} was not found in metadata.", _0)]
pub struct ImportMetadataNotFound(pub ImportId); pub struct ImportMetadataNotFound(pub import::Id);
/// Failed attempt to tread a file path as a module path. /// Failed attempt to tread a file path as a module path.
#[derive(Clone, Debug, Fail)] #[derive(Clone, Debug, Fail)]
@ -370,7 +370,7 @@ pub struct IdeMetadata {
#[serde(deserialize_with = "enso_prelude::deserialize_or_default")] #[serde(deserialize_with = "enso_prelude::deserialize_or_default")]
node: HashMap<ast::Id, NodeMetadata>, node: HashMap<ast::Id, NodeMetadata>,
#[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")] #[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")]
import: HashMap<ImportId, ImportMetadata>, import: HashMap<import::Id, ImportMetadata>,
/// The project metadata. This is stored only in the main module's metadata. /// The project metadata. This is stored only in the main module's metadata.
#[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")] #[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")]
project: Option<ProjectMetadata>, project: Option<ProjectMetadata>,
@ -610,15 +610,15 @@ pub trait API: Debug + model::undo_redo::Aware {
/// Modify metadata of given import. /// Modify metadata of given import.
fn with_import_metadata( fn with_import_metadata(
&self, &self,
id: ImportId, id: import::Id,
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>, fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
) -> FallibleResult; ) -> FallibleResult;
/// Returns the import metadata fof the module. /// Returns the import metadata fof the module.
fn all_import_metadata(&self) -> Vec<(ImportId, ImportMetadata)>; fn all_import_metadata(&self) -> Vec<(import::Id, ImportMetadata)>;
/// Removes the import metadata of the import. /// Removes the import metadata of the import.
fn remove_import_metadata(&self, id: ImportId) -> FallibleResult<ImportMetadata>; fn remove_import_metadata(&self, id: import::Id) -> FallibleResult<ImportMetadata>;
/// This method exists as a monomorphication for [`with_project_metadata`]. Users are encouraged /// This method exists as a monomorphication for [`with_project_metadata`]. Users are encouraged
/// to use it rather then this method. /// to use it rather then this method.

View File

@ -16,7 +16,7 @@ use crate::model::module::TextChange;
use crate::notification; use crate::notification;
use double_representation::definition::DefinitionInfo; use double_representation::definition::DefinitionInfo;
use double_representation::module::ImportId; use double_representation::import;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser_scala::api::ParsedSourceFile; use parser_scala::api::ParsedSourceFile;
use parser_scala::api::SourceFile; use parser_scala::api::SourceFile;
@ -212,7 +212,7 @@ impl model::module::API for Module {
fn with_import_metadata( fn with_import_metadata(
&self, &self,
id: ImportId, id: import::Id,
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>, fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
) -> FallibleResult { ) -> FallibleResult {
self.update_content(NotificationKind::MetadataChanged, |content| { self.update_content(NotificationKind::MetadataChanged, |content| {
@ -223,12 +223,12 @@ impl model::module::API for Module {
}) })
} }
fn all_import_metadata(&self) -> Vec<(ImportId, ImportMetadata)> { fn all_import_metadata(&self) -> Vec<(import::Id, ImportMetadata)> {
let content = self.content.borrow(); let content = self.content.borrow();
content.metadata.ide.import.clone().into_iter().collect() content.metadata.ide.import.clone().into_iter().collect()
} }
fn remove_import_metadata(&self, id: ImportId) -> FallibleResult<ImportMetadata> { fn remove_import_metadata(&self, id: import::Id) -> FallibleResult<ImportMetadata> {
self.try_updating_content(NotificationKind::MetadataChanged, |content| { self.try_updating_content(NotificationKind::MetadataChanged, |content| {
let lookup = content.metadata.ide.import.remove(&id); let lookup = content.metadata.ide.import.remove(&id);
lookup.ok_or_else(|| ImportMetadataNotFound(id).into()) lookup.ok_or_else(|| ImportMetadataNotFound(id).into())

View File

@ -16,7 +16,7 @@ use crate::model::module::API;
use ast::IdMap; use ast::IdMap;
use double_representation::definition::DefinitionInfo; use double_representation::definition::DefinitionInfo;
use double_representation::graph::Id; use double_representation::graph::Id;
use double_representation::module::ImportId; use double_representation::import;
use engine_protocol::language_server; use engine_protocol::language_server;
use engine_protocol::language_server::TextEdit; use engine_protocol::language_server::TextEdit;
use engine_protocol::types::Sha3_224; use engine_protocol::types::Sha3_224;
@ -255,17 +255,17 @@ impl API for Module {
fn with_import_metadata( fn with_import_metadata(
&self, &self,
id: ImportId, id: import::Id,
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>, fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
) -> FallibleResult { ) -> FallibleResult {
self.model.with_import_metadata(id, fun) self.model.with_import_metadata(id, fun)
} }
fn all_import_metadata(&self) -> Vec<(ImportId, ImportMetadata)> { fn all_import_metadata(&self) -> Vec<(import::Id, ImportMetadata)> {
self.model.all_import_metadata() self.model.all_import_metadata()
} }
fn remove_import_metadata(&self, id: ImportId) -> FallibleResult<ImportMetadata> { fn remove_import_metadata(&self, id: import::Id) -> FallibleResult<ImportMetadata> {
self.model.remove_import_metadata(id) self.model.remove_import_metadata(id)
} }