fix: module accessibility

This commit is contained in:
Shunsuke Shibayama 2023-05-21 22:35:44 +09:00
parent 379caacce9
commit 829b0d11b6
12 changed files with 191 additions and 25 deletions

View file

@ -50,7 +50,7 @@ impl<Checker: BuildRunnable> Server<Checker> {
if let Some(path) = self
.get_local_ctx(&uri, pos)
.first()
.and_then(|ctx| ctx.get_path_from_mod_t(&vi.t))
.and_then(|ctx| ctx.get_path_with_mod_t(&vi.t))
{
let mod_uri = Url::from_file_path(path).unwrap();
let resp = GotoDefinitionResponse::Array(vec![
@ -67,7 +67,7 @@ impl<Checker: BuildRunnable> Server<Checker> {
if let Some((_, vi)) = self
.get_local_ctx(&uri, pos)
.first()
.and_then(|ctx| ctx.get_mod_from_t(mod_t))
.and_then(|ctx| ctx.get_mod_with_t(mod_t))
.and_then(|mod_ctx| mod_ctx.get_var_info(token.inspect()))
{
let def_uri =

View file

@ -1,5 +1,74 @@
use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DirKind {
ErgModule,
PyModule,
Other,
NotDir,
}
impl From<&Path> for DirKind {
fn from(path: &Path) -> Self {
let Ok(dir) = path.read_dir() else {
return DirKind::NotDir;
};
for ent in dir {
let Ok(ent) = ent else {
continue;
};
if ent.path().file_name() == Some(OsStr::new("__init__.er")) {
return DirKind::ErgModule;
} else if ent.path().file_name() == Some(OsStr::new("__init__.py")) {
return DirKind::PyModule;
}
}
DirKind::Other
}
}
impl DirKind {
pub const fn is_erg_module(&self) -> bool {
matches!(self, DirKind::ErgModule)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileKind {
InitEr,
InitPy,
Er,
Py,
Other,
NotFile,
}
impl From<&Path> for FileKind {
fn from(path: &Path) -> Self {
if path.is_file() {
match path.file_name() {
Some(name) if name == OsStr::new("__init__.er") => FileKind::InitEr,
Some(name) if name == OsStr::new("__init__.py") => FileKind::InitPy,
Some(name) if name.to_string_lossy().ends_with(".er") => FileKind::Er,
Some(name) if name.to_string_lossy().ends_with(".py") => FileKind::Py,
_ => FileKind::Other,
}
} else {
FileKind::NotFile
}
}
}
impl FileKind {
pub const fn is_init_er(&self) -> bool {
matches!(self, FileKind::InitEr)
}
pub const fn is_simple_erg_file(&self) -> bool {
matches!(self, FileKind::Er)
}
}
pub fn is_cur_dir<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.components()

View file

@ -2455,6 +2455,12 @@ impl Context {
}
}
pub(crate) fn get_mod_with_path(&self, path: &Path) -> Option<&Context> {
(self.cfg.input.path() == Some(path)) // module itself
.then_some(self)
.or(self.mod_cache().get(path).map(|ent| &ent.module.context))
}
// FIXME: 現在の実装だとimportしたモジュールはどこからでも見れる
pub(crate) fn get_mod(&self, name: &str) -> Option<&Context> {
if name == "module" && ERG_MODE {
@ -2463,15 +2469,15 @@ impl Context {
self.get_builtins()
} else {
let t = self.get_var_info(name).map(|(_, vi)| &vi.t)?;
self.get_mod_from_t(t)
self.get_mod_with_t(t)
}
}
pub fn get_mod_from_t(&self, mod_t: &Type) -> Option<&Context> {
self.get_ctx_from_path(&self.get_path_from_mod_t(mod_t)?)
pub fn get_mod_with_t(&self, mod_t: &Type) -> Option<&Context> {
self.get_ctx_from_path(&self.get_path_with_mod_t(mod_t)?)
}
pub fn get_path_from_mod_t(&self, mod_t: &Type) -> Option<PathBuf> {
pub fn get_path_with_mod_t(&self, mod_t: &Type) -> Option<PathBuf> {
let tps = mod_t.typarams();
let Some(TyParam::Value(ValueObj::Str(path))) = tps.get(0) else {
return None;

View file

@ -10,6 +10,7 @@ use erg_common::consts::PYTHON_MODE;
use erg_common::env::is_pystd_main_module;
use erg_common::erg_util::BUILTIN_ERG_MODS;
use erg_common::levenshtein::get_similar_name;
use erg_common::pathutil::{DirKind, FileKind};
use erg_common::python_util::BUILTIN_PYTHON_MODS;
use erg_common::set::Set;
use erg_common::traits::{Locational, Stream};
@ -931,7 +932,9 @@ impl Context {
let Ok(mod_name) = hir::Literal::try_from(mod_name.token.clone()) else {
return Ok(());
};
let res = self.import_mod(call.additional_operation().unwrap(), &mod_name);
let res = self
.import_mod(call.additional_operation().unwrap(), &mod_name)
.map(|_path| ());
let arg = TyParam::Value(ValueObj::Str(
mod_name.token.content.replace('\"', "").into(),
));
@ -946,14 +949,14 @@ impl Context {
params: vec![arg],
}
};
let Some(ident) = def.sig.ident() else { return Ok(()) };
let Some(ident) = def.sig.ident() else { return res };
let Some((_, vi)) = self.get_var_info(ident.inspect()) else {
return Ok(());
return res;
};
if let Some(_fv) = vi.t.as_free() {
vi.t.link(&typ);
}
res.map(|_| ())
res
}
pub(crate) fn preregister_def(&mut self, def: &ast::Def) -> TyCheckResult<()> {
@ -1807,24 +1810,73 @@ impl Context {
}
fn import_erg_mod(&self, __name__: &Str, loc: &impl Locational) -> CompileResult<PathBuf> {
let mod_cache = self.mod_cache();
let path = match self.cfg.input.resolve_real_path(Path::new(&__name__[..])) {
Some(path) => path,
None => {
return Err(self.import_err(__name__, loc));
}
};
// module itself
if self.cfg.input.path() == Some(path.as_path()) {
return Ok(path);
}
self.check_mod_vis(path.as_path(), __name__, loc)?;
if let Some(referrer) = self.cfg.input.path() {
let graph = &self.shared.as_ref().unwrap().graph;
graph.inc_ref(referrer, path.clone());
}
if mod_cache.get(&path).is_some() {
if self.get_mod_with_path(&path).is_some() {
return Ok(path);
}
self.build_erg_mod(path, __name__, loc)
}
/// If the path is like `foo/bar`, check if `bar` is a public module (the definition is in `foo/__init__.er`)
fn check_mod_vis(
&self,
path: &Path,
__name__: &Str,
loc: &impl Locational,
) -> CompileResult<()> {
if let Some(parent) = path.parent() {
if FileKind::from(path).is_simple_erg_file() && DirKind::from(parent).is_erg_module() {
let parent = parent.join("__init__.er");
let parent_module = if let Some(parent) = self.get_mod_with_path(&parent) {
Some(parent)
} else {
// TODO: This error should not be discarded, but the later processing should also continue while generating the error
self.build_erg_mod(parent.clone(), __name__, loc)?;
self.get_mod_with_path(&parent)
};
if let Some(parent_module) = parent_module {
let import_err = || {
TyCheckErrors::from(TyCheckError::import_error(
self.cfg.input.clone(),
line!() as usize,
format!("module `{__name__}` is not public"),
loc.loc(),
self.caused_by(),
None,
None,
))
};
let mod_name = path.file_stem().unwrap_or_default().to_string_lossy();
if let Some((_, vi)) = parent_module.get_var_info(&mod_name) {
if !vi.vis.compatible(&ast::AccessModifier::Public, self) {
return Err(import_err());
}
} else {
return Err(import_err());
}
}
}
}
Ok(())
}
fn build_erg_mod(
&self,
path: PathBuf,
__name__: &Str,
loc: &impl Locational,
) -> CompileResult<PathBuf> {
let mod_cache = self.mod_cache();
let mut cfg = self.cfg.inherit(path.clone());
let src = cfg
.input
@ -1839,15 +1891,15 @@ impl Context {
Some(artifact.object),
builder.pop_mod_ctx().unwrap(),
);
Ok(path)
}
Err(artifact) => {
if let Some(hir) = artifact.object {
mod_cache.register(path, Some(hir), builder.pop_mod_ctx().unwrap());
}
return Err(artifact.errors);
Err(artifact.errors)
}
}
Ok(path)
}
fn similar_builtin_py_mod_name(&self, name: &Str) -> Option<Str> {
@ -1974,6 +2026,16 @@ impl Context {
if py_mod_cache.get(&path).is_some() {
return Ok(path);
}
self.build_decl_mod(path, __name__, loc)
}
fn build_decl_mod(
&self,
path: PathBuf,
__name__: &Str,
loc: &impl Locational,
) -> CompileResult<PathBuf> {
let py_mod_cache = self.py_mod_cache();
let mut cfg = self.cfg.inherit(path.clone());
let src = cfg
.input
@ -1988,15 +2050,15 @@ impl Context {
Ok(artifact) => {
let ctx = builder.pop_mod_ctx().unwrap();
py_mod_cache.register(path.clone(), Some(artifact.object), ctx);
Ok(path)
}
Err(artifact) => {
if let Some(hir) = artifact.object {
py_mod_cache.register(path, Some(hir), builder.pop_mod_ctx().unwrap());
}
return Err(artifact.errors);
Err(artifact.errors)
}
}
Ok(path)
}
pub fn del(&mut self, ident: &hir::Identifier) -> CompileResult<()> {

View file

@ -42,12 +42,19 @@ Now the `bar` directory is recognized as a module. If the only file in `bar` is
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
└─┬ qux
└─ __init__.er
```
```python
# bar/__init__.er
.baz = import "./baz"
.qux = import "./qux"
```
From outside the `bar` directory, you can use like the following.
```erg
```python
bar = import "bar"
bar.baz.p!()
@ -55,11 +62,12 @@ bar.qux.p!()
```
`__init__.er` is not just a marker that makes a directory as a module, it also controls the visibility of the module.
Unlike Python, Erg modules are not importable by default.
```erg
```python
# __init__.er
# `. /` points to the current directory. It can be omitted
# `./` points to the current directory. It can be omitted
.baz = import ". /baz"
qux = import ". /qux"

View file

@ -44,7 +44,14 @@ assert foo.i == 1
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
└─┬ qux
└─ __init__.er
```
```python
# bar/__init__.er
.baz = import "./baz"
.qux = import "./qux"
```
`bar`ディレクトリの外側からは以下のようにして使用できます。
@ -57,6 +64,7 @@ bar.qux.p!()
```
`__init__.er`は単にディレクトリをモジュールとして機能させるだけのマーカーではなく、モジュールの可視性を制御する役割も持ちます。
Pythonとは違い、Ergのモジュールはデフォルトではインポートできないようになっています。
```python
# __init__.er

View file

@ -0,0 +1 @@
.bar = import "./bar"

View file

@ -0,0 +1 @@
.i = 1

View file

@ -0,0 +1 @@
.i = 1

View file

@ -0,0 +1 @@
.i = 1

View file

@ -0,0 +1,4 @@
_ = import "foo" # OK
_ = import "foo/bar" # OK
_ = import "foo/baz" # ERR
_ = import "foo/qux" # ERR

View file

@ -283,6 +283,11 @@ fn exec_impl_err() -> Result<(), ()> {
expect_failure("tests/should_err/impl.er", 2, 2)
}
#[test]
fn exec_import_err() -> Result<(), ()> {
expect_failure("tests/should_err/import.er", 0, 2)
}
#[test]
fn exec_infer_union_array() -> Result<(), ()> {
expect_failure("tests/should_err/infer_union_array.er", 2, 1)