Add nested import syntax (#4228)

Co-authored-by: LuizAugustoPapa <luiz.papa@aluno.puc-rio.br>
Co-authored-by: PepinhoJp <pepinho.jp@gmail.com>
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
This commit is contained in:
Tulio Martins 2024-05-30 04:56:40 -03:00 committed by GitHub
parent 5f6d942519
commit 06a925a0ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 150 additions and 23 deletions

View File

@ -2040,29 +2040,54 @@ impl<'a> ImportItems<'a> {
pub fn iter(self) -> impl DoubleEndedIterator<Item = ImportItem<'a>> {
self.0.children().filter_map(|child| match child.kind() {
SyntaxKind::RenamedImportItem => child.cast().map(ImportItem::Renamed),
SyntaxKind::Ident => child.cast().map(ImportItem::Simple),
SyntaxKind::ImportItemPath => child.cast().map(ImportItem::Simple),
_ => Option::None,
})
}
}
node! {
/// A path to a submodule's imported name: `a.b.c`.
ImportItemPath
}
impl<'a> ImportItemPath<'a> {
/// An iterator over the path's components.
pub fn iter(self) -> impl DoubleEndedIterator<Item = Ident<'a>> {
self.0.children().filter_map(SyntaxNode::cast)
}
/// The name of the imported item. This is the last segment in the path.
pub fn name(self) -> Ident<'a> {
self.iter().last().unwrap_or_default()
}
}
/// An imported item, potentially renamed to another identifier.
#[derive(Debug, Copy, Clone, Hash)]
pub enum ImportItem<'a> {
/// A non-renamed import (the item's name in the scope is the same as its
/// name).
Simple(Ident<'a>),
Simple(ImportItemPath<'a>),
/// A renamed import (the item was bound to a different name in the scope
/// than the one it was defined as).
Renamed(RenamedImportItem<'a>),
}
impl<'a> ImportItem<'a> {
/// The path to the imported item.
pub fn path(self) -> ImportItemPath<'a> {
match self {
Self::Simple(path) => path,
Self::Renamed(renamed_item) => renamed_item.path(),
}
}
/// The original name of the imported item, at its source. This will be the
/// equal to the bound name if the item wasn't renamed with 'as'.
pub fn original_name(self) -> Ident<'a> {
match self {
Self::Simple(name) => name,
Self::Simple(path) => path.name(),
Self::Renamed(renamed_item) => renamed_item.original_name(),
}
}
@ -2071,7 +2096,7 @@ impl<'a> ImportItem<'a> {
/// name, if it was renamed; otherwise, it's just its original name.
pub fn bound_name(self) -> Ident<'a> {
match self {
Self::Simple(name) => name,
Self::Simple(path) => path.name(),
Self::Renamed(renamed_item) => renamed_item.new_name(),
}
}
@ -2083,17 +2108,22 @@ node! {
}
impl<'a> RenamedImportItem<'a> {
/// The original name of the imported item (`a` in `a as d`).
pub fn original_name(self) -> Ident<'a> {
/// The path to the imported item.
pub fn path(self) -> ImportItemPath<'a> {
self.0.cast_first_match().unwrap_or_default()
}
/// The original name of the imported item (`a` in `a as d` or `c.b.a as d`).
pub fn original_name(self) -> Ident<'a> {
self.path().name()
}
/// The new name of the imported item (`d` in `a as d`).
pub fn new_name(self) -> Ident<'a> {
self.0
.children()
.filter_map(SyntaxNode::cast)
.nth(1)
.last()
.unwrap_or_default()
}
}

View File

@ -277,6 +277,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::ForLoop => None,
SyntaxKind::ModuleImport => None,
SyntaxKind::ImportItems => None,
SyntaxKind::ImportItemPath => None,
SyntaxKind::RenamedImportItem => None,
SyntaxKind::ModuleInclude => None,
SyntaxKind::LoopBreak => None,

View File

@ -262,6 +262,8 @@ pub enum SyntaxKind {
ModuleImport,
/// Items to import from a module: `a, b, c`.
ImportItems,
/// A path to an imported name from a submodule: `a.b.c`.
ImportItemPath,
/// A renamed import item: `a as d`.
RenamedImportItem,
/// A module include: `include "chapter1.typ"`.
@ -488,6 +490,7 @@ impl SyntaxKind {
Self::ForLoop => "for-loop expression",
Self::ModuleImport => "`import` expression",
Self::ImportItems => "import items",
Self::ImportItemPath => "imported item path",
Self::RenamedImportItem => "renamed import item",
Self::ModuleInclude => "`include` expression",
Self::LoopBreak => "`break` expression",

View File

@ -1003,6 +1003,13 @@ fn import_items(p: &mut Parser) {
p.unexpected();
}
// Nested import path: `a.b.c`
while p.eat_if(SyntaxKind::Dot) {
p.expect(SyntaxKind::Ident);
}
p.wrap(item_marker, SyntaxKind::ImportItemPath);
// Rename imported item.
if p.eat_if(SyntaxKind::As) {
p.expect(SyntaxKind::Ident);

View File

@ -63,23 +63,62 @@ impl Eval for ast::ModuleImport<'_> {
Some(ast::Imports::Items(items)) => {
let mut errors = eco_vec![];
for item in items.iter() {
let original_ident = item.original_name();
if let Some(value) = scope.get(&original_ident) {
// Warn on `import ...: x as x`
if let ast::ImportItem::Renamed(renamed_item) = &item {
if renamed_item.original_name().as_str()
== renamed_item.new_name().as_str()
{
vm.engine.tracer.warn(warning!(
renamed_item.new_name().span(),
"unnecessary import rename to same name",
));
}
}
let mut path = item.path().iter().peekable();
let mut scope = scope;
vm.define(item.bound_name(), value.clone());
} else {
errors.push(error!(original_ident.span(), "unresolved import"));
while let Some(component) = &path.next() {
let Some(value) = scope.get(component) else {
errors.push(error!(component.span(), "unresolved import"));
break;
};
if path.peek().is_some() {
// Nested import, as this is not the last component.
// This must be a submodule.
let Some(submodule) = value.scope() else {
let error = if matches!(value, Value::Func(function) if function.scope().is_none())
{
error!(
component.span(),
"cannot import from user-defined functions"
)
} else if !matches!(
value,
Value::Func(_) | Value::Module(_) | Value::Type(_)
) {
error!(
component.span(),
"expected module, function, or type, found {}",
value.ty()
)
} else {
panic!("unexpected nested import failure")
};
errors.push(error);
break;
};
// Walk into the submodule.
scope = submodule;
} else {
// Now that we have the scope of the innermost submodule
// in the import path, we may extract the desired item from
// it.
// Warn on `import ...: x as x`
if let ast::ImportItem::Renamed(renamed_item) = &item {
if renamed_item.original_name().as_str()
== renamed_item.new_name().as_str()
{
vm.engine.tracer.warn(warning!(
renamed_item.new_name().span(),
"unnecessary import rename to same name",
));
}
}
vm.define(item.bound_name(), value.clone());
}
}
}
if !errors.is_empty() {

View File

@ -45,6 +45,16 @@
#test(val, 1)
#test(other(1, 2), 3)
--- import-nested-item ---
// Nested item imports.
#import "modules/chap1.typ" as orig-chap1
#import "modules/chap2.typ" as orig-chap2
#import "module.typ": chap2, chap2.name, chap2.chap1, chap2.chap1.name as othername
#test(chap2, orig-chap2)
#test(chap1, orig-chap1)
#test(name, "Klaus")
#test(othername, "Klaus")
--- import-from-function-scope ---
// Test importing from function scopes.
@ -63,6 +73,28 @@
#import assert: eq as aseq
#aseq(10, 10)
--- import-from-function-scope-nested-import ---
// Test importing items from function scopes via nested import.
#import std: grid.cell, table.cell as tcell
#test(cell, grid.cell)
#test(tcell, table.cell)
--- import-from-type-scope ---
// Test importing from a type's scope.
#import array: zip
#test(zip((1, 2), (3, 4)), ((1, 3), (2, 4)))
--- import-from-type-scope-item-renamed ---
// Test importing from a type's scope with renaming.
#import array: pop as renamed-pop
#test(renamed-pop((1, 2)), 2)
--- import-from-type-scope-nested-import ---
// Test importing from a type's scope with nested import.
#import std: array.zip, array.pop as renamed-pop
#test(zip((1, 2), (3, 4)), ((1, 3), (2, 4)))
#test(renamed-pop((1, 2)), 2)
--- import-from-file-bare ---
// A module import without items.
#import "module.typ"
@ -225,6 +257,10 @@ This is never reached.
// Error: 7-12 unknown variable: chap1
#test(chap1.b, "Klaus")
--- import-nested-invalid-type ---
// Error: 19-21 expected module, function, or type, found float
#import std: calc.pi.something
--- import-incomplete ---
// Error: 8 expected expression
#import
@ -262,6 +298,15 @@ This is never reached.
// Error: 16-17 unexpected integer
#import "": a: 1
--- import-incomplete-nested ---
// Error: 15 expected identifier
#import "": a.
--- import-wildcard-in-nested ---
// Error: 15 expected identifier
// Error: 15-16 unexpected star
#import "": a.*
--- import-missing-comma ---
// Error: 14 expected comma
#import "": a b

View File

@ -1,5 +1,6 @@
// SKIP
// A file to import in import / include tests.
#import "modules/chap2.typ"
#let a
#let b = 1

View File

@ -1,4 +1,5 @@
// SKIP
#import "chap1.typ"
#let name = "Klaus"
== Chapter 2