feat: dynamic analysis on import from dynamic expressions (#233)

* feat: dynamic analysis on import from dynamic expressions

* dev: adds more fixture
This commit is contained in:
Myriad-Dreamin 2024-05-05 17:39:56 +08:00 committed by GitHub
parent 46f524de57
commit abb89ed3e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 308 additions and 158 deletions

View file

@ -1,17 +1,12 @@
//! Static analysis for def-use relations.
use std::{
collections::HashMap,
ops::{Deref, Range},
sync::Arc,
};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ecow::EcoVec;
use log::info;
use super::{prelude::*, ImportInfo};
use crate::adt::snapshot_map::SnapshotMap;
use crate::syntax::find_source_by_import_path;
/// The type namespace of def-use relations
///
@ -118,7 +113,7 @@ pub(super) fn get_def_use_inner(
ctx: &mut AnalysisContext,
source: Source,
e: EcoVec<LexicalHierarchy>,
_m: Arc<ImportInfo>,
import: Arc<ImportInfo>,
) -> Option<Arc<DefUseInfo>> {
let current_id = source.id();
@ -127,6 +122,7 @@ pub(super) fn get_def_use_inner(
info: DefUseInfo::default(),
id_scope: SnapshotMap::default(),
label_scope: SnapshotMap::default(),
import,
current_id,
ext_src: None,
@ -143,6 +139,7 @@ struct DefUseCollector<'a, 'w> {
info: DefUseInfo,
label_scope: SnapshotMap<String, DefId>,
id_scope: SnapshotMap<String, DefId>,
import: Arc<ImportInfo>,
current_id: TypstFileId,
ext_src: Option<Source>,
@ -203,7 +200,6 @@ impl<'a, 'w> DefUseCollector<'a, 'w> {
for e in e {
match &e.info.kind {
LexicalKind::Heading(..) => unreachable!(),
LexicalKind::Mod(LexicalModKind::PathInclude) => {}
LexicalKind::Var(LexicalVarKind::Label) => {
self.insert(Ns::Label, e);
}
@ -212,6 +208,37 @@ impl<'a, 'w> DefUseCollector<'a, 'w> {
| LexicalKind::Var(LexicalVarKind::Variable) => {
self.insert(Ns::Value, e);
}
LexicalKind::Var(LexicalVarKind::ValRef) => self.insert_ref(Ns::Value, e),
LexicalKind::Block => {
if let Some(e) = &e.children {
self.enter(|this| this.scan(e.as_slice()))?;
}
}
LexicalKind::Mod(LexicalModKind::Module(..)) => {
let mut src = self.import.imports.get(&e.info.range)?.clone();
info!("check import: {info:?} => {src:?}", info = e.info);
std::mem::swap(&mut self.ext_src, &mut src);
// todo: process import star
if let Some(e) = &e.children {
self.scan(e.as_slice())?;
}
std::mem::swap(&mut self.ext_src, &mut src);
}
LexicalKind::Mod(LexicalModKind::Star) => {
if let Some(source) = &self.ext_src {
info!("diving source for def use: {:?}", source.id());
let (_, external_info) =
Some(source.id()).zip(self.ctx.def_use(source.clone()))?;
for ext_id in &external_info.exports_refs {
self.import_from(&external_info, *ext_id);
}
}
}
LexicalKind::Mod(LexicalModKind::PathInclude) => {}
LexicalKind::Mod(LexicalModKind::PathVar)
| LexicalKind::Mod(LexicalModKind::ModuleAlias) => self.insert_module(Ns::Value, e),
LexicalKind::Mod(LexicalModKind::Ident) => match self.import_name(&e.info.name) {
@ -245,43 +272,6 @@ impl<'a, 'w> DefUseCollector<'a, 'w> {
}
}
}
LexicalKind::Var(LexicalVarKind::ValRef) => self.insert_ref(Ns::Value, e),
LexicalKind::Block => {
if let Some(e) = &e.children {
self.enter(|this| this.scan(e.as_slice()))?;
}
}
LexicalKind::Mod(LexicalModKind::Module(p)) => {
match p {
ModSrc::Expr(_) => {}
ModSrc::Path(p) => {
let src = find_source_by_import_path(
self.ctx.world(),
self.current_id,
p.deref(),
);
self.ext_src = src;
}
}
// todo: process import star
if let Some(e) = &e.children {
self.scan(e.as_slice())?;
}
self.ext_src = None;
}
LexicalKind::Mod(LexicalModKind::Star) => {
if let Some(source) = &self.ext_src {
info!("diving source for def use: {:?}", source.id());
let (_, external_info) =
Some(source.id()).zip(self.ctx.def_use(source.clone()))?;
for ext_id in &external_info.exports_refs {
self.import_from(&external_info, *ext_id);
}
}
}
}
}

View file

@ -40,8 +40,9 @@ use crate::{
#[derive(Default)]
pub struct ModuleAnalysisCache {
source: OnceCell<FileResult<Source>>,
top_level_eval: OnceCell<Option<Arc<TypeCheckInfo>>>,
import_info: OnceCell<Option<Arc<ImportInfo>>>,
def_use: OnceCell<Option<Arc<DefUseInfo>>>,
type_check: OnceCell<Option<Arc<TypeCheckInfo>>>,
}
impl ModuleAnalysisCache {
@ -52,6 +53,19 @@ impl ModuleAnalysisCache {
.clone()
}
/// Try to get the def-use information of a file.
pub fn import_info(&self) -> Option<Arc<ImportInfo>> {
self.import_info.get().cloned().flatten()
}
/// Compute the def-use information of a file.
pub(crate) fn compute_import(
&self,
f: impl FnOnce() -> Option<Arc<ImportInfo>>,
) -> Option<Arc<ImportInfo>> {
self.import_info.get_or_init(f).clone()
}
/// Try to get the def-use information of a file.
pub fn def_use(&self) -> Option<Arc<DefUseInfo>> {
self.def_use.get().cloned().flatten()
@ -65,17 +79,17 @@ impl ModuleAnalysisCache {
self.def_use.get_or_init(f).clone()
}
/// Try to get the top-level evaluation information of a file.
/// Try to get the type check information of a file.
pub(crate) fn type_check(&self) -> Option<Arc<TypeCheckInfo>> {
self.top_level_eval.get().cloned().flatten()
self.type_check.get().cloned().flatten()
}
/// Compute the top-level evaluation information of a file.
/// Compute the type check information of a file.
pub(crate) fn compute_type_check(
&self,
f: impl FnOnce() -> Option<Arc<TypeCheckInfo>>,
) -> Option<Arc<TypeCheckInfo>> {
self.top_level_eval.get_or_init(f).clone()
self.type_check.get_or_init(f).clone()
}
}
@ -593,6 +607,41 @@ impl<'w> AnalysisContext<'w> {
res
}
/// Get the import information of a source file.
pub fn import_info(&mut self, source: Source) -> Option<Arc<ImportInfo>> {
let fid = source.id();
if let Some(res) = self.caches.modules.entry(fid).or_default().import_info() {
return Some(res);
}
let cache = self.at_module(fid);
let l = cache
.def_use_lexical_hierarchy
.compute(source.clone(), |_before, after| {
cache.signatures.clear();
crate::syntax::get_lexical_hierarchy(after, crate::syntax::LexicalScopeKind::DefUse)
})
.ok()
.flatten()?;
let res = cache
.import
.clone()
.compute(l.clone(), |_before, after| {
crate::analysis::get_import_info(self, source, after)
})
.ok()
.flatten();
self.caches
.modules
.entry(fid)
.or_default()
.compute_import(|| res.clone());
res
}
/// Get the def-use information of a source file.
pub fn def_use(&mut self, source: Source) -> Option<Arc<DefUseInfo>> {
let fid = source.id();
@ -611,15 +660,7 @@ impl<'w> AnalysisContext<'w> {
.ok()
.flatten()?;
let source2 = source.clone();
let m = cache
.import
.clone()
.compute(l.clone(), |_before, after| {
crate::analysis::get_import_info(self, source2, after)
})
.ok()
.flatten()?;
let m = self.import_info(source.clone())?;
let cache = self.at_module(fid);
let res = cache

View file

@ -1,14 +1,21 @@
//! Import analysis
use ecow::EcoVec;
use typst::{
foundations::Value,
syntax::{LinkedNode, SyntaxKind},
};
use crate::syntax::find_source_by_import_path;
use crate::syntax::resolve_id_by_path;
use super::analyze_import;
pub use super::prelude::*;
/// The import information of a source file.
#[derive(Default)]
pub struct ImportInfo {
/// The source files that this source file depends on.
pub deps: EcoVec<TypstFileId>,
/// The source file that this source file imports.
pub imports: indexmap::IndexMap<Range<usize>, Option<Source>>,
}
@ -28,16 +35,28 @@ pub(super) fn get_import_info(
e: EcoVec<LexicalHierarchy>,
) -> Option<Arc<ImportInfo>> {
let current_id = source.id();
let root = LinkedNode::new(source.root());
let mut collector = ImportCollector {
ctx,
info: ImportInfo::default(),
current_id,
root,
};
collector.scan(&e);
let mut deps: Vec<_> = collector
.info
.imports
.values()
.filter_map(|x| x.as_ref().map(|x| x.id()))
.collect();
deps.sort();
deps.dedup();
collector.info.deps = deps.into();
Some(Arc::new(collector.info))
}
@ -46,6 +65,7 @@ struct ImportCollector<'a, 'w> {
info: ImportInfo,
current_id: TypstFileId,
root: LinkedNode<'a>,
}
impl<'a, 'w> ImportCollector<'a, 'w> {
@ -67,18 +87,60 @@ impl<'a, 'w> ImportCollector<'a, 'w> {
| LexicalModKind::Alias { .. }
| LexicalModKind::Star,
) => {}
LexicalKind::Mod(LexicalModKind::Module(p)) => match p {
ModSrc::Expr(_) => {}
ModSrc::Path(p) => {
let src = find_source_by_import_path(
self.ctx.world(),
self.current_id,
p.deref(),
);
self.info.imports.insert(e.info.range.clone(), src);
}
},
LexicalKind::Mod(LexicalModKind::Module(p)) => {
let id = match p {
ModSrc::Expr(exp) => {
let exp = find_import_expr(self.root.leaf_at(exp.range.end));
let val = exp
.as_ref()
.and_then(|exp| analyze_import(self.ctx.world(), exp));
match val {
Some(Value::Module(m)) => {
log::debug!(
"current id {:?} exp {:?} => id: {:?}",
self.current_id,
exp,
m.file_id()
);
m.file_id()
}
Some(Value::Str(m)) => resolve_id_by_path(
self.ctx.world(),
self.current_id,
m.as_str(),
),
_ => None,
}
}
ModSrc::Path(p) => {
resolve_id_by_path(self.ctx.world(), self.current_id, p.deref())
}
};
log::debug!(
"current id {:?} range {:?} => id: {:?}",
self.current_id,
e.info.range,
id
);
let source = id.and_then(|id| self.ctx.source_by_id(id).ok());
self.info.imports.insert(e.info.range.clone(), source);
}
}
}
}
}
fn find_import_expr(end: Option<LinkedNode>) -> Option<LinkedNode> {
let mut node = end?;
while let Some(parent) = node.parent() {
if matches!(
parent.kind(),
SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude
) {
return Some(node);
}
node = parent.clone();
}
None
}

View file

@ -0,0 +1,9 @@
// path: base.typ
#let f() = 1;
-----
// path: derive.typ
#import "base.typ"
-----
#import "derive.typ": *
#import base: *
#f()

View file

@ -0,0 +1,31 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/import_by_ident.typ
---
{
"base@8..18@derive.typ": {
"def": {
"kind": {
"Mod": "PathVar"
},
"name": "base",
"range": "8:18"
},
"refs": [
"base@32..36"
]
},
"f@5..6@base.typ": {
"def": {
"kind": {
"Var": "Function"
},
"name": "f",
"range": "5:6"
},
"refs": [
"f@41..42"
]
}
}

View file

@ -0,0 +1,61 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/import_by_ident.typ
---
[
{
"children": [
{
"kind": {
"Mod": "Star"
},
"name": "*",
"range": "22:23"
}
],
"kind": {
"Mod": {
"Module": {
"Path": "derive.typ"
}
}
},
"name": "",
"range": "1:23"
},
{
"children": [
{
"kind": {
"Var": "ValRef"
},
"name": "base",
"range": "32:36"
},
{
"kind": {
"Mod": "Star"
},
"name": "*",
"range": "38:39"
}
],
"kind": {
"Mod": {
"Module": {
"Expr": "@32..36"
}
}
},
"name": "",
"range": "25:39"
},
{
"kind": {
"Var": "ValRef"
},
"name": "f",
"range": "41:42"
}
]

View file

@ -0,0 +1,18 @@
// path: base.typ
#let tmpl(content, authors: (), font: none, class: "article") = {
if class != "article" and class != "letter" {
panic("")
}
set document(author: authors)
set text(font: font)
set page(paper: "a4") if class == "article"
set page(paper: "us-letter") if class == "letter"
content
}
-----
#import "base.typ": *
#tmpl(class: /* position */ )[]

View file

@ -1,6 +1,7 @@
use crate::prelude::*;
fn resolve_id_by_path(
/// Resolve a file id by its import path.
pub fn resolve_id_by_path(
world: &dyn World,
current: TypstFileId,
import_path: &str,
@ -28,17 +29,6 @@ fn resolve_id_by_path(
Some(TypstFileId::new(current.package().cloned(), vpath))
}
/// Find a source instance by its import path.
pub fn find_source_by_import_path(
world: &dyn World,
current: TypstFileId,
import_path: &str,
) -> Option<Source> {
world
.source(resolve_id_by_path(world, current, import_path)?)
.ok()
}
/// Find a source instance by its import node.
pub fn find_source_by_expr(
world: &dyn World,
@ -47,63 +37,9 @@ pub fn find_source_by_expr(
) -> Option<Source> {
// todo: this could be valid: import("path.typ"), where v is parenthesized
match e {
ast::Expr::Str(s) => find_source_by_import_path(world, current, s.get().as_str()),
ast::Expr::Str(s) => world
.source(resolve_id_by_path(world, current, s.get().as_str())?)
.ok(),
_ => None,
}
}
/// Find all static imports in a source.
pub fn find_imports(world: &dyn World, source: &Source) -> EcoVec<TypstFileId> {
let root = LinkedNode::new(source.root());
let mut worker = ImportWorker {
world,
current: source.id(),
imports: EcoVec::new(),
};
worker.analyze(root);
let res = worker.imports;
let mut res: Vec<TypstFileId> = res.into_iter().map(|(id, _)| id).collect();
res.sort();
res.dedup();
res.into_iter().collect()
}
struct ImportWorker<'a> {
world: &'a dyn World,
current: TypstFileId,
imports: EcoVec<(TypstFileId, LinkedNode<'a>)>,
}
impl<'a> ImportWorker<'a> {
fn analyze(&mut self, node: LinkedNode<'a>) -> Option<()> {
match node.kind() {
SyntaxKind::ModuleImport => {
let i = node.cast::<ast::ModuleImport>().unwrap();
let src = i.source();
match src {
ast::Expr::Str(s) => {
// todo: source in packages
let s = s.get();
let id = resolve_id_by_path(self.world, self.current, s.as_str())?;
self.imports.push((id, node));
}
// todo: handle dynamic import
ast::Expr::FieldAccess(..) | ast::Expr::Ident(..) => {}
_ => {}
}
return None;
}
SyntaxKind::ModuleInclude => {}
_ => {}
}
for child in node.children() {
self.analyze(child);
}
None
}
}

View file

@ -98,7 +98,7 @@ impl<'de> Deserialize<'de> for IdentRef {
/// A flat and transient reference to some symbol in a source file.
///
/// See [`IdentRef`] for definition of a "transient" reference.
#[derive(Serialize, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct IdentDef {
/// The name of the symbol.
pub name: String,

View file

@ -3,7 +3,6 @@ use std::sync::Once;
use once_cell::sync::Lazy;
use regex::RegexSet;
use super::find_imports;
use crate::prelude::*;
/// The dependency information of a module (file).
@ -37,14 +36,17 @@ pub fn construct_module_dependencies(
};
let file_id = source.id();
let deps = find_imports(ctx.world(), &source);
let Some(import) = ctx.import_info(source) else {
continue;
};
dependencies
.entry(file_id)
.or_insert_with(|| ModuleDependency {
dependencies: deps.clone(),
dependencies: import.deps.clone(),
dependents: EcoVec::default(),
});
for dep in deps {
for dep in import.deps.clone() {
dependents
.entry(dep)
.or_insert_with(EcoVec::new)