mirror of
https://github.com/enso-org/enso.git
synced 2024-11-26 08:52:58 +03:00
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:
parent
3a8aa90f1b
commit
c4a7e28fb5
307
app/gui/controller/double-representation/src/import.rs
Normal file
307
app/gui/controller/double-representation/src/import.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
#![feature(iter_order_by)]
|
||||
#![feature(option_result_contains)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(iter_next_chunk)]
|
||||
// === Standard Linter Configuration ===
|
||||
#![deny(non_ascii_idents)]
|
||||
#![warn(unsafe_code)]
|
||||
@ -43,6 +44,7 @@ pub mod connection;
|
||||
pub mod definition;
|
||||
pub mod graph;
|
||||
pub mod identifier;
|
||||
pub mod import;
|
||||
pub mod module;
|
||||
pub mod node;
|
||||
pub mod project;
|
||||
|
@ -10,6 +10,7 @@ use crate::identifier;
|
||||
use crate::identifier::Identifier;
|
||||
use crate::identifier::LocatedName;
|
||||
use crate::identifier::ReferentName;
|
||||
use crate::import;
|
||||
use crate::project;
|
||||
use crate::tp;
|
||||
|
||||
@ -31,12 +32,12 @@ use serde::Serialize;
|
||||
#[derive(Clone, Debug, Fail)]
|
||||
#[fail(display = "Import `{}` was not found in the module.", _0)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ImportNotFound(pub ImportInfo);
|
||||
pub struct ImportNotFound(pub import::Info);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Fail)]
|
||||
#[fail(display = "Import with ID `{}` was not found in the module.", _0)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ImportIdNotFound(pub ImportId);
|
||||
pub struct ImportIdNotFound(pub import::Id);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Fail)]
|
||||
#[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 ===
|
||||
// ============
|
||||
@ -514,15 +438,15 @@ impl Info {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
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.
|
||||
///
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@ -548,7 +472,7 @@ impl Info {
|
||||
///
|
||||
/// If there is more than one line matching, only the first one will be removed.
|
||||
/// 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 (crumb, _) = lookup_result.ok_or_else(|| ImportNotFound(to_remove.clone()))?;
|
||||
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.
|
||||
/// 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 (crumb, _) = lookup_result.ok_or(ImportIdNotFound(to_remove))?;
|
||||
self.remove_line(crumb.line_index)?;
|
||||
@ -573,10 +497,10 @@ impl Info {
|
||||
// TODO [mwu]
|
||||
// Ideally we should not require parser but should use some sane way of generating AST from
|
||||
// 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.
|
||||
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 import_ast = parser.parse_line_ast(to_add.to_string()).unwrap();
|
||||
@ -592,7 +516,7 @@ impl Info {
|
||||
to_add: &QualifiedName,
|
||||
) {
|
||||
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);
|
||||
if !is_here && !already_imported {
|
||||
self.add_import(parser, import);
|
||||
@ -885,7 +809,7 @@ mod tests {
|
||||
let imports = info.iter_imports().collect_vec();
|
||||
assert_eq!(imports.len(), expected.len());
|
||||
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 import = |code| {
|
||||
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"));
|
||||
@ -915,13 +839,13 @@ mod tests {
|
||||
info.add_import(&parser, import("import 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.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.remove_import(&ImportInfo::from_target_str("Gar.Bar")).unwrap();
|
||||
info.remove_import(&import("import Gar.Bar")).unwrap();
|
||||
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.add_import(&parser, import("import Bar.Gar"));
|
||||
|
@ -1113,7 +1113,7 @@ pub trait HasRepr {
|
||||
|
||||
/// Check if the representation is empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() >= 0.bytes()
|
||||
self.len() <= 0.bytes()
|
||||
}
|
||||
|
||||
/// Get the representation length in chars.
|
||||
|
@ -207,25 +207,32 @@ pub fn ast_as_import_match(ast: &Ast) -> Option<known::Match> {
|
||||
is_match_import(¯o_match).then_some(macro_match)
|
||||
}
|
||||
|
||||
/// Check if the given macro match node is an import declaration.
|
||||
pub fn is_match_import(ast: &known::Match) -> bool {
|
||||
/// If the given AST node is a qualified import declaration (`import <module name>`), returns it as
|
||||
/// 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 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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn is_ast_import(ast: &Ast) -> bool {
|
||||
ast_as_import_match(ast).is_some()
|
||||
|
@ -412,7 +412,9 @@ impl Fixture {
|
||||
];
|
||||
|
||||
for macro_usage in macro_usages.iter() {
|
||||
println!(">>>>>>>>>> {}", macro_usage);
|
||||
let ast = self.parser.parse_line_ast(*macro_usage).unwrap();
|
||||
println!("{:?}", ast);
|
||||
expect_shape::<Match<Ast>>(&ast);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use crate::model::module::TextChange;
|
||||
|
||||
use ast;
|
||||
use ast::HasIdMap;
|
||||
use double_representation::import;
|
||||
use double_representation::module;
|
||||
use double_representation::project;
|
||||
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.
|
||||
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))?;
|
||||
Ok(())
|
||||
}
|
||||
@ -163,12 +164,12 @@ impl Handle {
|
||||
///
|
||||
/// Fails, if there was no such declaration found.
|
||||
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))?
|
||||
}
|
||||
|
||||
/// 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();
|
||||
module.iter_imports().collect()
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use breadcrumbs::Breadcrumbs;
|
||||
use const_format::concatcp;
|
||||
use double_representation::graph::GraphInfo;
|
||||
use double_representation::graph::LocationHint;
|
||||
use double_representation::module::ImportInfo;
|
||||
use double_representation::import;
|
||||
use double_representation::module::QualifiedName;
|
||||
use double_representation::node::NodeInfo;
|
||||
use double_representation::project;
|
||||
@ -1058,7 +1058,7 @@ impl Searcher {
|
||||
let without_enso_project = imports.filter(|i| i.to_string() != ENSO_PROJECT_SPECIAL_MODULE);
|
||||
for mut import in without_enso_project {
|
||||
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);
|
||||
if already_exists {
|
||||
@ -1319,7 +1319,10 @@ impl Searcher {
|
||||
Some(module_name)
|
||||
} else {
|
||||
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 imported_names = module_info
|
||||
.iter_imports()
|
||||
.map(|import| import.qualified_name().unwrap())
|
||||
.map(|import| import.qualified_module_name().unwrap())
|
||||
.collect_vec();
|
||||
|
||||
let expected_import = expected_import.into_iter().cloned().collect_vec();
|
||||
|
@ -8,7 +8,7 @@ use ast::constants::LANGUAGE_FILE_EXTENSION;
|
||||
use ast::constants::SOURCE_DIRECTORY;
|
||||
use double_representation::definition::DefinitionInfo;
|
||||
use double_representation::identifier::ReferentName;
|
||||
use double_representation::module::ImportId;
|
||||
use double_representation::import;
|
||||
use double_representation::project;
|
||||
use engine_protocol::language_server::MethodPointer;
|
||||
use flo_stream::Subscriber;
|
||||
@ -44,7 +44,7 @@ pub struct NodeMetadataNotFound(pub ast::Id);
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, Fail)]
|
||||
#[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.
|
||||
#[derive(Clone, Debug, Fail)]
|
||||
@ -370,7 +370,7 @@ pub struct IdeMetadata {
|
||||
#[serde(deserialize_with = "enso_prelude::deserialize_or_default")]
|
||||
node: HashMap<ast::Id, NodeMetadata>,
|
||||
#[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.
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")]
|
||||
project: Option<ProjectMetadata>,
|
||||
@ -610,15 +610,15 @@ pub trait API: Debug + model::undo_redo::Aware {
|
||||
/// Modify metadata of given import.
|
||||
fn with_import_metadata(
|
||||
&self,
|
||||
id: ImportId,
|
||||
id: import::Id,
|
||||
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
|
||||
) -> FallibleResult;
|
||||
|
||||
/// 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.
|
||||
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
|
||||
/// to use it rather then this method.
|
||||
|
@ -16,7 +16,7 @@ use crate::model::module::TextChange;
|
||||
use crate::notification;
|
||||
|
||||
use double_representation::definition::DefinitionInfo;
|
||||
use double_representation::module::ImportId;
|
||||
use double_representation::import;
|
||||
use flo_stream::Subscriber;
|
||||
use parser_scala::api::ParsedSourceFile;
|
||||
use parser_scala::api::SourceFile;
|
||||
@ -212,7 +212,7 @@ impl model::module::API for Module {
|
||||
|
||||
fn with_import_metadata(
|
||||
&self,
|
||||
id: ImportId,
|
||||
id: import::Id,
|
||||
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
|
||||
) -> FallibleResult {
|
||||
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();
|
||||
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| {
|
||||
let lookup = content.metadata.ide.import.remove(&id);
|
||||
lookup.ok_or_else(|| ImportMetadataNotFound(id).into())
|
||||
|
@ -16,7 +16,7 @@ use crate::model::module::API;
|
||||
use ast::IdMap;
|
||||
use double_representation::definition::DefinitionInfo;
|
||||
use double_representation::graph::Id;
|
||||
use double_representation::module::ImportId;
|
||||
use double_representation::import;
|
||||
use engine_protocol::language_server;
|
||||
use engine_protocol::language_server::TextEdit;
|
||||
use engine_protocol::types::Sha3_224;
|
||||
@ -255,17 +255,17 @@ impl API for Module {
|
||||
|
||||
fn with_import_metadata(
|
||||
&self,
|
||||
id: ImportId,
|
||||
id: import::Id,
|
||||
fun: Box<dyn FnOnce(&mut ImportMetadata) + '_>,
|
||||
) -> FallibleResult {
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user