From 3d88169b32e4eb4f23647971c04b58273044a3c9 Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Wed, 9 Oct 2024 18:32:17 +0800 Subject: [PATCH 1/9] feat: add models and relations --- .gitignore | 1 + Cargo.lock | 77 +++++++- Cargo.toml | 2 + crates/cli/src/main.rs | 7 +- crates/dt_core/Cargo.toml | 1 + crates/dt_core/src/lib.rs | 4 + crates/dt_database/Cargo.toml | 11 ++ crates/dt_database/src/lib.rs | 89 ++++++++++ crates/dt_database/src/models.rs | 296 +++++++++++++++++++++++++++++++ 9 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 crates/dt_database/Cargo.toml create mode 100644 crates/dt_database/src/lib.rs create mode 100644 crates/dt_database/src/models.rs diff --git a/.gitignore b/.gitignore index 1cc4cd6..19851c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /outputs +/database \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5288dc1..168b4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -52,7 +52,7 @@ dependencies = [ "actix-utils", "ahash 0.8.11", "base64 0.22.1", - "bitflags 2.5.0", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -469,9 +469,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" @@ -994,6 +994,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" name = "dt_core" version = "0.1.0" dependencies = [ + "dt_database", "dt_graph", "dt_i18n", "dt_parser", @@ -1004,6 +1005,14 @@ dependencies = [ "dt_tracker", ] +[[package]] +name = "dt_database" +version = "0.1.0" +dependencies = [ + "anyhow", + "rusqlite", +] + [[package]] name = "dt_graph" version = "0.1.0" @@ -1149,6 +1158,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.13.0" @@ -1348,6 +1369,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -1613,6 +1643,17 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2141,7 +2182,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -2238,6 +2279,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2274,7 +2329,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2667,7 +2722,7 @@ version = "0.119.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69e9696b3d02197c16ba7548c95b31f7ca79532200d269ce3ad03a5b2174cf28" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytecheck", "is-macro", "num-bigint", @@ -2753,7 +2808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1e55ce789bd4411b1e0a8b83149c70dd1186e38471fd65860dcece8a522f2f" dependencies = [ "better_scoped_tls", - "bitflags 2.5.0", + "bitflags 2.6.0", "indexmap", "once_cell", "phf", @@ -3332,6 +3387,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vergen" version = "9.0.1" diff --git a/Cargo.toml b/Cargo.toml index 1e34545..3abbc87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/cli", "crates/demo", "crates/dt_core", + "crates/dt_database", "crates/dt_graph", "crates/dt_i18n", "crates/dt_parser", @@ -23,3 +24,4 @@ serde_json = "1.0" swc_core = { version = "0.104.2", features = ["common", "ecma_ast", "ecma_visit", "ecma_plugin_transform"] } swc_ecma_parser = { version = "0.150.0", features = ["typescript"] } clap = { version = "4.5", features = ["derive"] } +rusqlite = { version = "0.32.1", features = ["bundled"] } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e28fd76..ad2b655 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,6 +1,7 @@ use anyhow::Context; use clap::Parser; use dt_core::{ + database::{models, Database, SqliteDb}, graph::{depend_on_graph::DependOnGraph, used_by_graph::UsedByGraph}, i18n::I18nToSymbol, parser::{collect_symbol_dependency, Input}, @@ -36,15 +37,19 @@ fn main() -> anyhow::Result<()> { let project_root = PathBuf::from(&cli.input).to_canonical_string()?; let translation_json = File::open(&cli.translation_path)?; let translation_json_reader = BufReader::new(translation_json); - let mut scheduler = ParserCandidateScheduler::new(&project_root); let mut depend_on_graph = DependOnGraph::new(&project_root); let mut symbol_to_route = SymbolToRoutes::new(); let mut i18n_to_symbol = I18nToSymbol::new(); + + let db = SqliteDb::open("./database/1009.db3")?; + db.create_tables()?; + let project = models::Project::create(&db.conn, &project_root)?; loop { match scheduler.get_one_candidate() { Some(c) => { let module_src = c.to_str().context(format!("to_str() failed: {:?}", c))?; + let module = project.add_module(&db.conn, module_src)?; let module_ast = Input::Path(module_src).get_module_ast()?; let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; diff --git a/crates/dt_core/Cargo.toml b/crates/dt_core/Cargo.toml index 7019982..dbf5c97 100644 --- a/crates/dt_core/Cargo.toml +++ b/crates/dt_core/Cargo.toml @@ -7,6 +7,7 @@ version = "0.1.0" [dependencies] +dt_database = { version = "0.1.0", path = "../dt_database" } dt_graph = { version = "0.1.0", path = "../dt_graph" } dt_i18n = { version = "0.1.0", path = "../dt_i18n" } dt_parser = { version = "0.1.0", path = "../dt_parser" } diff --git a/crates/dt_core/src/lib.rs b/crates/dt_core/src/lib.rs index 96d243b..ea55815 100644 --- a/crates/dt_core/src/lib.rs +++ b/crates/dt_core/src/lib.rs @@ -1,3 +1,7 @@ +pub mod database { + pub use dt_database::*; +} + pub mod graph { pub use dt_graph::*; } diff --git a/crates/dt_database/Cargo.toml b/crates/dt_database/Cargo.toml new file mode 100644 index 0000000..476e996 --- /dev/null +++ b/crates/dt_database/Cargo.toml @@ -0,0 +1,11 @@ +[package] +authors = ["Leo Lin "] +description = "relational database for dt" +edition = "2021" +name = "dt_database" +version = "0.1.0" + + +[dependencies] +anyhow = { workspace = true } +rusqlite = { workspace = true } diff --git a/crates/dt_database/src/lib.rs b/crates/dt_database/src/lib.rs new file mode 100644 index 0000000..4776223 --- /dev/null +++ b/crates/dt_database/src/lib.rs @@ -0,0 +1,89 @@ +pub mod models; + +use models::{ + Model, Module, Project, Route, RouteUsage, Symbol, SymbolDependency, Translation, + TranslationUsage, +}; +use rusqlite::Connection; +use std::path::Path; + +pub trait Database { + fn open(path: impl AsRef) -> anyhow::Result + where + Self: Sized; + fn create_tables(&self) -> anyhow::Result<()>; +} + +#[derive(Debug)] +pub struct SqliteDb { + pub conn: Connection, +} + +impl SqliteDb { + fn create_table_if_not_exists(&self, table: &str) -> anyhow::Result<()> { + let sql = format!("CREATE TABLE if not exists {}", table); + self.conn.execute(&sql, ())?; + + Ok(()) + } +} + +impl Database for SqliteDb { + fn open(path: impl AsRef) -> anyhow::Result { + Ok(Self { + conn: Connection::open(path)?, + }) + } + + fn create_tables(&self) -> anyhow::Result<()> { + self.create_table_if_not_exists(&Project::table())?; + self.create_table_if_not_exists(&Module::table())?; + self.create_table_if_not_exists(&Symbol::table())?; + self.create_table_if_not_exists(&SymbolDependency::table())?; + self.create_table_if_not_exists(&Translation::table())?; + self.create_table_if_not_exists(&TranslationUsage::table())?; + self.create_table_if_not_exists(&Route::table())?; + self.create_table_if_not_exists(&RouteUsage::table())?; + + Ok(()) + } +} + +#[test] +fn db_works() { + let db = + SqliteDb::open("/Users/linweitang/rust/js-symbol-dependency-tracker/database/test.db3") + .unwrap(); + db.create_tables().unwrap(); + println!("{:#?}", db); + + for project in db + .conn + .prepare("SELECT * FROM project") + .unwrap() + .query_map([], Project::from_row) + .unwrap() + { + println!("{:?}", project.unwrap()); + } + + for module in db + .conn + .prepare("SELECT * FROM module") + .unwrap() + .query_map([], Module::from_row) + .unwrap() + { + println!("{:?}", module.unwrap()); + } + + for symbol in db + .conn + .prepare("SELECT * FROM symbol") + .unwrap() + .query_map([], Symbol::from_row) + .unwrap() + { + println!("{:?}", symbol.unwrap()); + } +} diff --git a/crates/dt_database/src/models.rs b/crates/dt_database/src/models.rs new file mode 100644 index 0000000..3cedb46 --- /dev/null +++ b/crates/dt_database/src/models.rs @@ -0,0 +1,296 @@ +use rusqlite::{params, Connection, Row}; + +pub trait Model { + fn table() -> String; +} + +#[derive(Debug)] +pub struct Project { + pub id: usize, + pub path: String, +} + +impl Model for Project { + fn table() -> String { + " + project ( + id INTEGER PRIMARY KEY, + path TEXT NOT NULL + ) + " + .to_string() + } +} + +impl Project { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + path: row.get(1)?, + }) + } + + pub fn create(conn: &Connection, path: &str) -> anyhow::Result { + conn.execute("INSERT INTO project (path) VALUES (?1)", params![path])?; + let id = conn.query_row("SELECT last_insert_rowid()", (), |row| { + Ok(row.get::<_, usize>(0)?) + })?; + let project = conn.query_row( + "SELECT * FROM project where id=?1", + params![id], + Self::from_row, + )?; + Ok(project) + } + + pub fn add_module(&self, conn: &Connection, path: &str) -> anyhow::Result { + Module::create(conn, self, path) + } +} + +#[derive(Debug)] +pub struct Module { + pub id: usize, + pub project_id: usize, + pub path: String, +} + +impl Model for Module { + fn table() -> String { + " + module ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER REFERENCES project(id) ON DELETE CASCADE, + path TEXT NOT NULL + ) + " + .to_string() + } +} + +impl Module { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + project_id: row.get(1)?, + path: row.get(2)?, + }) + } + + pub fn create(conn: &Connection, project: &Project, path: &str) -> anyhow::Result { + conn.execute( + "INSERT INTO module (project_id, path) VALUES (?1, ?2)", + params![project.id, path], + )?; + let id = conn.query_row("SELECT last_insert_rowid()", (), |row| { + Ok(row.get::<_, usize>(0)?) + })?; + let module = conn.query_row( + "SELECT * FROM module where id=?1", + params![id], + Self::from_row, + )?; + Ok(module) + } +} + +#[derive(Debug)] +pub enum SymbolVariant { + LocalVariable, + NamedExport, + DefaultExport, +} + +impl SymbolVariant { + pub fn from(n: usize) -> Self { + match n { + 0 => Self::LocalVariable, + 1 => Self::NamedExport, + 2 => Self::DefaultExport, + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +pub struct Symbol { + pub id: usize, + pub module_id: usize, + pub variant: SymbolVariant, + pub name: String, +} + +impl Model for Symbol { + fn table() -> String { + " + symbol ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + module_id INTEGER REFERENCES module(id) ON DELETE CASCADE, + variant INTEGER NOT NULL, + name TEXT NOT NULL + ) + " + .to_string() + } +} + +impl Symbol { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + module_id: row.get(1)?, + variant: SymbolVariant::from(row.get::<_, usize>(2)?), + name: row.get(3)?, + }) + } +} + +// Join Table +#[derive(Debug)] +pub struct SymbolDependency { + pub id: usize, + pub symbol_id: usize, + pub depend_on_symbol_id: usize, +} + +impl Model for SymbolDependency { + fn table() -> String { + " + symbol_dependency ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE, + depend_on_symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE + ) + " + .to_string() + } +} + +impl SymbolDependency { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + symbol_id: row.get(1)?, + depend_on_symbol_id: row.get(2)?, + }) + } +} + +#[derive(Debug)] +pub struct Translation { + pub id: usize, + pub key: String, + pub value: String, +} + +impl Model for Translation { + fn table() -> String { + " + translation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL, + value TEXT NOT NULL + ) + " + .to_string() + } +} + +impl Translation { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + key: row.get(1)?, + value: row.get(2)?, + }) + } +} + +// Join Table +#[derive(Debug)] +pub struct TranslationUsage { + pub id: usize, + pub translation_id: usize, + pub symbol_id: usize, +} + +impl Model for TranslationUsage { + fn table() -> String { + " + translation_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + translation_id INTEGER REFERENCES translation(id) ON DELETE CASCADE, + symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE + ) + " + .to_string() + } +} + +impl TranslationUsage { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + translation_id: row.get(1)?, + symbol_id: row.get(2)?, + }) + } +} + +#[derive(Debug)] +pub struct Route { + pub id: usize, + pub path: String, +} + +impl Model for Route { + fn table() -> String { + " + route ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT NOT NULL + ) + " + .to_string() + } +} + +impl Route { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + path: row.get(1)?, + }) + } +} + +// Join Table +#[derive(Debug)] +pub struct RouteUsage { + pub id: usize, + pub route_id: usize, + pub symbol_id: usize, +} + +impl Model for RouteUsage { + fn table() -> String { + " + route_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + translation_id INTEGER REFERENCES route(id) ON DELETE CASCADE, + symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE + ) + " + .to_string() + } +} + +impl RouteUsage { + pub fn from_row(row: &Row) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + route_id: row.get(1)?, + symbol_id: row.get(2)?, + }) + } +} From 27ab333609acff3532d16c94008eb35fc61f3b17 Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Thu, 10 Oct 2024 22:57:08 +0800 Subject: [PATCH 2/9] feat: create records for symbol dependency of each parsed module --- crates/cli/src/main.rs | 333 ++++++++++++++++++++++++++++++- crates/dt_database/src/models.rs | 136 +++++++++++-- 2 files changed, 451 insertions(+), 18 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ad2b655..be304f0 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -4,8 +4,13 @@ use dt_core::{ database::{models, Database, SqliteDb}, graph::{depend_on_graph::DependOnGraph, used_by_graph::UsedByGraph}, i18n::I18nToSymbol, - parser::{collect_symbol_dependency, Input}, - path_resolver::ToCanonicalString, + parser::{ + anonymous_default_export::SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT, + collect_symbol_dependency, + types::{FromOtherModule, FromType, ModuleExport, ModuleScopedVariable, SymbolDependency}, + Input, + }, + path_resolver::{PathResolver, ToCanonicalString}, portable::Portable, route::SymbolToRoutes, scheduler::ParserCandidateScheduler, @@ -32,9 +37,328 @@ struct Cli { output: String, } +struct Project { + db: SqliteDb, + project_root: String, + project: models::Project, + path_resolver: PathResolver, +} + +impl Project { + pub fn new(project_root: &str, db_path: &str) -> anyhow::Result { + let db = SqliteDb::open(db_path)?; + db.create_tables()?; + let project = models::Project::create(&db.conn, project_root)?; + Ok(Self { + db, + project_root: project_root.to_owned(), + project, + path_resolver: PathResolver::new(project_root), + }) + } + + fn remove_prefix(&self, canonical_path: &str) -> String { + match canonical_path.starts_with(&self.project_root) { + true => canonical_path[self.project_root.len()..].to_string(), + false => canonical_path.to_string(), + } + } + + fn resolve_path(&self, current_path: &str, import_src: &str) -> anyhow::Result { + Ok(self.remove_prefix(&self.path_resolver.resolve_path(current_path, import_src)?)) + } + + fn handle_local_variable_table( + &self, + module: &models::Module, + symbol_dependency: &SymbolDependency, + ) -> anyhow::Result<()> { + for ( + symbol_name, + ModuleScopedVariable { + depend_on, + import_from, + }, + ) in symbol_dependency.local_variable_table.iter() + { + let current_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + symbol_name, + )?; + if let Some(depend_on) = depend_on { + // Items in depend_on vector is guranteed to be local variables of the same module. + // So we can create those symbols as local variable. + for depend_on_symbol_name in depend_on.iter() { + let depend_on_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + } + if let Some(FromOtherModule { from, from_type }) = import_from { + if let Ok(from) = self.resolve_path(&symbol_dependency.canonical_path, &from) { + let import_from_module = + self.project.get_or_create_module(&self.db.conn, &from)?; + // It's ok to create a named export or default export symbol for other module + // even that module hasn't been parsed yet. + match from_type { + dt_core::parser::types::FromType::Named(depend_on_symbol_name) => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::NamedExport, + &depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + dt_core::parser::types::FromType::Default => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::DefaultExport, + "", // default export doesn't have name + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + dt_core::parser::types::FromType::Namespace => { + // When A module import namespace from B module, B module is guranteed to be + // parsed before A module. So we can query all named exports from B module. + let named_export_symbols = + import_from_module.get_named_export_symbols(&self.db.conn)?; + for depend_on_symbol in named_export_symbols.iter() { + if depend_on_symbol.name != SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT + { + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + } + } + }; + } + } + } + Ok(()) + } + + fn handle_named_export_table( + &self, + module: &models::Module, + symbol_dependency: &SymbolDependency, + ) -> anyhow::Result<()> { + for (exported_symbol_name, exported_from) in symbol_dependency.named_export_table.iter() { + let current_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::NamedExport, + &exported_symbol_name, + )?; + match exported_from { + ModuleExport::Local(depend_on_symbol_name) => { + let depend_on_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + &depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + ModuleExport::ReExportFrom(FromOtherModule { from, from_type }) => { + if let Ok(from) = self.resolve_path(&symbol_dependency.canonical_path, &from) { + let import_from_module = + self.project.get_or_create_module(&self.db.conn, &from)?; + // It's ok to create a named export or default export symbol for other module + // even that module hasn't been parsed yet. + match from_type { + dt_core::parser::types::FromType::Named(depend_on_symbol_name) => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::NamedExport, + &depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + dt_core::parser::types::FromType::Default => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::DefaultExport, + "", // default export doesn't have name + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + dt_core::parser::types::FromType::Namespace => { + // When A module import namespace from B module, B module is guranteed to be + // parsed before A module. So we can query all named exports from B module. + let named_export_symbols = + import_from_module.get_named_export_symbols(&self.db.conn)?; + for depend_on_symbol in named_export_symbols.iter() { + if depend_on_symbol.name + != SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT + { + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + } + } + }; + } + } + } + } + Ok(()) + } + + fn handle_default_export( + &self, + module: &models::Module, + symbol_dependency: &SymbolDependency, + ) -> anyhow::Result<()> { + if let Some(default_export) = symbol_dependency.default_export.as_ref() { + let current_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::DefaultExport, + "", // default export doesn't have name + )?; + match default_export { + ModuleExport::Local(depend_on_symbol_name) => { + let depend_on_symbol = module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + &depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + ModuleExport::ReExportFrom(FromOtherModule { from, from_type }) => { + if let Ok(from) = self.resolve_path(&symbol_dependency.canonical_path, &from) { + let import_from_module = + self.project.get_or_create_module(&self.db.conn, &from)?; + // It's ok to create a named export or default export symbol for other module + // even that module hasn't been parsed yet. + match from_type { + dt_core::parser::types::FromType::Named(depend_on_symbol_name) => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::NamedExport, + &depend_on_symbol_name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + dt_core::parser::types::FromType::Default => { + let depend_on_symbol = import_from_module.get_or_create_symbol( + &self.db.conn, + models::SymbolVariant::DefaultExport, + "", // default export doesn't have name + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + FromType::Namespace => { + unreachable!( + "can't not export namespace from other module as default export" + ) + } + } + } + } + } + } + + Ok(()) + } + + fn handle_re_export_star_from( + &self, + module: &models::Module, + symbol_dependency: &SymbolDependency, + ) -> anyhow::Result<()> { + if let Some(re_export_start_from) = symbol_dependency.re_export_star_from.as_ref() { + for from in re_export_start_from.iter() { + if let Ok(from) = self.resolve_path(&symbol_dependency.canonical_path, &from) { + // When A module do wildcard export from B module, B module is guranteed to be + // parsed before A module. So we can query all named exports from B module. + let import_from_module = self.project.get_module(&self.db.conn, &from)?; + let named_export_symbols = + import_from_module.get_named_export_symbols(&self.db.conn)?; + for depend_on_symbol in named_export_symbols.iter() { + // Create a named export symbol for this module, and set the dependency to + // the named export symbol of imported module. + if depend_on_symbol.name != SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT { + let current_symbol = module.add_symbol( + &self.db.conn, + models::SymbolVariant::NamedExport, + &depend_on_symbol.name, + )?; + models::SymbolDependency::create( + &self.db.conn, + ¤t_symbol, + &depend_on_symbol, + )?; + } + } + } + } + } + Ok(()) + } + + pub fn add_module(&self, symbol_dependency: &SymbolDependency) -> anyhow::Result<()> { + let module = self.project.add_module( + &self.db.conn, + &self.remove_prefix(&symbol_dependency.canonical_path), + )?; + + self.handle_local_variable_table(&module, symbol_dependency)?; + self.handle_named_export_table(&module, symbol_dependency)?; + self.handle_default_export(&module, symbol_dependency)?; + self.handle_re_export_star_from(&module, symbol_dependency)?; + + Ok(()) + } +} + fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let project_root = PathBuf::from(&cli.input).to_canonical_string()?; + let project = Project::new(&project_root, "./database/1010.db3")?; let translation_json = File::open(&cli.translation_path)?; let translation_json_reader = BufReader::new(translation_json); let mut scheduler = ParserCandidateScheduler::new(&project_root); @@ -42,19 +366,16 @@ fn main() -> anyhow::Result<()> { let mut symbol_to_route = SymbolToRoutes::new(); let mut i18n_to_symbol = I18nToSymbol::new(); - let db = SqliteDb::open("./database/1009.db3")?; - db.create_tables()?; - let project = models::Project::create(&db.conn, &project_root)?; loop { match scheduler.get_one_candidate() { Some(c) => { let module_src = c.to_str().context(format!("to_str() failed: {:?}", c))?; - let module = project.add_module(&db.conn, module_src)?; let module_ast = Input::Path(module_src).get_module_ast()?; let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; + project.add_module(&symbol_dependency)?; depend_on_graph.add_symbol_dependency(symbol_dependency)?; scheduler.mark_candidate_as_parsed(c); } diff --git a/crates/dt_database/src/models.rs b/crates/dt_database/src/models.rs index 3cedb46..fc147aa 100644 --- a/crates/dt_database/src/models.rs +++ b/crates/dt_database/src/models.rs @@ -1,4 +1,4 @@ -use rusqlite::{params, Connection, Row}; +use rusqlite::{params, Connection, Row, ToSql}; pub trait Model { fn table() -> String; @@ -30,14 +30,12 @@ impl Project { }) } + /// single thread only: last_insert_rowid() pub fn create(conn: &Connection, path: &str) -> anyhow::Result { conn.execute("INSERT INTO project (path) VALUES (?1)", params![path])?; - let id = conn.query_row("SELECT last_insert_rowid()", (), |row| { - Ok(row.get::<_, usize>(0)?) - })?; let project = conn.query_row( - "SELECT * FROM project where id=?1", - params![id], + "SELECT * FROM project WHERE id=last_insert_rowid()", + (), Self::from_row, )?; Ok(project) @@ -46,6 +44,18 @@ impl Project { pub fn add_module(&self, conn: &Connection, path: &str) -> anyhow::Result { Module::create(conn, self, path) } + + pub fn get_module(&self, conn: &Connection, path: &str) -> anyhow::Result { + Module::retrieve(conn, self, path) + } + + /// single thread only: retrieve then create + pub fn get_or_create_module(&self, conn: &Connection, path: &str) -> anyhow::Result { + match Module::retrieve(conn, self, path) { + Ok(module) => Ok(module), + Err(_) => self.add_module(conn, path), + } + } } #[derive(Debug)] @@ -77,24 +87,65 @@ impl Module { }) } + /// single thread only: last_insert_rowid() pub fn create(conn: &Connection, project: &Project, path: &str) -> anyhow::Result { conn.execute( "INSERT INTO module (project_id, path) VALUES (?1, ?2)", params![project.id, path], )?; - let id = conn.query_row("SELECT last_insert_rowid()", (), |row| { - Ok(row.get::<_, usize>(0)?) - })?; let module = conn.query_row( - "SELECT * FROM module where id=?1", - params![id], + "SELECT * FROM module WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(module) + } + + pub fn retrieve(conn: &Connection, project: &Project, path: &str) -> anyhow::Result { + let module = conn.query_row( + "SELECT * FROM module WHERE (project_id, path) = (?1, ?2)", + params![project.id, path], Self::from_row, )?; Ok(module) } + + pub fn add_symbol( + &self, + conn: &Connection, + variant: SymbolVariant, + name: &str, + ) -> anyhow::Result { + Symbol::create(conn, self, variant, name) + } + + /// single thread only: retrieve then create + pub fn get_or_create_symbol( + &self, + conn: &Connection, + variant: SymbolVariant, + name: &str, + ) -> anyhow::Result { + match Symbol::retrieve(conn, self, variant, name) { + Ok(symbol) => Ok(symbol), + Err(_) => self.add_symbol(conn, variant, name), + } + } + + pub fn get_named_export_symbols(&self, conn: &Connection) -> anyhow::Result> { + let named_export_symbols: Vec = conn + .prepare("SELECT * FROM symbol WHERE (module_id, variant) = (?1, ?2)")? + .query_map( + params![self.id, SymbolVariant::NamedExport], + Symbol::from_row, + )? + .map(|s| s.unwrap()) + .collect(); + Ok(named_export_symbols) + } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum SymbolVariant { LocalVariable, NamedExport, @@ -112,6 +163,16 @@ impl SymbolVariant { } } +impl ToSql for SymbolVariant { + fn to_sql(&self) -> rusqlite::Result> { + match self { + SymbolVariant::LocalVariable => 0.to_sql(), + SymbolVariant::NamedExport => 1.to_sql(), + SymbolVariant::DefaultExport => 2.to_sql(), + } + } +} + #[derive(Debug)] pub struct Symbol { pub id: usize, @@ -143,6 +204,39 @@ impl Symbol { name: row.get(3)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create( + conn: &Connection, + module: &Module, + variant: SymbolVariant, + name: &str, + ) -> anyhow::Result { + conn.execute( + "INSERT INTO symbol (module_id, variant, name) VALUES (?1, ?2, ?3)", + params![module.id, variant, name], + )?; + let symbol = conn.query_row( + "SELECT * FROM symbol WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(symbol) + } + + pub fn retrieve( + conn: &Connection, + module: &Module, + variant: SymbolVariant, + name: &str, + ) -> anyhow::Result { + let symbol = conn.query_row( + "SELECT * FROM symbol WHERE (module_id, variant, name) = (?1, ?2, ?3)", + params![module.id, variant, name], + Self::from_row, + )?; + Ok(symbol) + } } // Join Table @@ -174,6 +268,24 @@ impl SymbolDependency { depend_on_symbol_id: row.get(2)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create( + conn: &Connection, + current_symbol: &Symbol, + depend_on_symbol: &Symbol, + ) -> anyhow::Result { + conn.execute( + "INSERT INTO symbol_dependency (symbol_id, depend_on_symbol_id) VALUES (?1, ?2)", + params![current_symbol.id, depend_on_symbol.id], + )?; + let symbol_dependency = conn.query_row( + "SELECT * FROM symbol_dependency WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(symbol_dependency) + } } #[derive(Debug)] From c393e3d4c8f30d2141eada7b5b5fb3c13c39ae4b Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 00:06:40 +0800 Subject: [PATCH 3/9] feat: create records for translation and i18n usage of each parsed module --- crates/cli/src/main.rs | 72 +++++++++++++++++++++++++--- crates/dt_database/src/models.rs | 81 ++++++++++++++++++++++++++++++-- crates/dt_i18n/src/collect.rs | 6 +-- 3 files changed, 145 insertions(+), 14 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index be304f0..797faf6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -16,6 +16,7 @@ use dt_core::{ scheduler::ParserCandidateScheduler, }; use std::{ + collections::{HashMap, HashSet}, fs::File, io::{prelude::*, BufReader}, path::PathBuf, @@ -340,7 +341,10 @@ impl Project { Ok(()) } - pub fn add_module(&self, symbol_dependency: &SymbolDependency) -> anyhow::Result<()> { + pub fn add_module( + &self, + symbol_dependency: &SymbolDependency, + ) -> anyhow::Result { let module = self.project.add_module( &self.db.conn, &self.remove_prefix(&symbol_dependency.canonical_path), @@ -351,6 +355,46 @@ impl Project { self.handle_default_export(&module, symbol_dependency)?; self.handle_re_export_star_from(&module, symbol_dependency)?; + Ok(module) + } + + pub fn add_translation( + &self, + translation_json: &HashMap, + ) -> anyhow::Result<()> { + for (key, value) in translation_json.iter() { + self.project.add_translation(&self.db.conn, key, value)?; + } + Ok(()) + } + + pub fn add_i18n_usage( + &self, + module: &models::Module, + i18n_usage: &HashMap>, + ) -> anyhow::Result<()> { + for (symbol_name, i18n_keys) in i18n_usage.iter() { + let symbol = module + .get_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + &symbol_name, + ) + .context(format!( + "try to add i18n keys for symbol {}, but symbol doesn't exist", + symbol_name, + ))?; + for key in i18n_keys.iter() { + let translation = + self.project + .get_translation(&self.db.conn, key) + .context(format!( + "try to add translation for symbol {}, but translation {} doesn't exist", + symbol_name, key + ))?; + models::TranslationUsage::create(&self.db.conn, &translation, &symbol)?; + } + } Ok(()) } } @@ -359,23 +403,39 @@ fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let project_root = PathBuf::from(&cli.input).to_canonical_string()?; let project = Project::new(&project_root, "./database/1010.db3")?; - let translation_json = File::open(&cli.translation_path)?; - let translation_json_reader = BufReader::new(translation_json); + let translation_file = File::open(&cli.translation_path)?; + let translation_json_reader = BufReader::new(translation_file); let mut scheduler = ParserCandidateScheduler::new(&project_root); let mut depend_on_graph = DependOnGraph::new(&project_root); let mut symbol_to_route = SymbolToRoutes::new(); let mut i18n_to_symbol = I18nToSymbol::new(); + let translation_json: HashMap = + serde_json::from_reader(translation_json_reader)?; + project + .add_translation(&translation_json) + .context("add translation to project")?; + loop { match scheduler.get_one_candidate() { Some(c) => { let module_src = c.to_str().context(format!("to_str() failed: {:?}", c))?; let module_ast = Input::Path(module_src).get_module_ast()?; let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; - i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; + let i18n_usage = i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; - project.add_module(&symbol_dependency)?; + let module = project.add_module(&symbol_dependency).context(format!( + "add module {} to project", + symbol_dependency.canonical_path + ))?; + project + .add_i18n_usage(&module, &i18n_usage) + .context(format!( + "add i18n usage of module {} to project", + symbol_dependency.canonical_path + ))?; + depend_on_graph.add_symbol_dependency(symbol_dependency)?; scheduler.mark_candidate_as_parsed(c); } @@ -385,7 +445,7 @@ fn main() -> anyhow::Result<()> { let portable = Portable::new( project_root.to_owned(), - serde_json::from_reader(translation_json_reader)?, + translation_json, i18n_to_symbol.table, symbol_to_route.table, UsedByGraph::from(&depend_on_graph), diff --git a/crates/dt_database/src/models.rs b/crates/dt_database/src/models.rs index fc147aa..619dbae 100644 --- a/crates/dt_database/src/models.rs +++ b/crates/dt_database/src/models.rs @@ -56,6 +56,19 @@ impl Project { Err(_) => self.add_module(conn, path), } } + + pub fn add_translation( + &self, + conn: &Connection, + key: &str, + value: &str, + ) -> anyhow::Result { + Translation::create(conn, self, key, value) + } + + pub fn get_translation(&self, conn: &Connection, key: &str) -> anyhow::Result { + Translation::retrieve(conn, self, key) + } } #[derive(Debug)] @@ -119,6 +132,15 @@ impl Module { Symbol::create(conn, self, variant, name) } + pub fn get_symbol( + &self, + conn: &Connection, + variant: SymbolVariant, + name: &str, + ) -> anyhow::Result { + Symbol::retrieve(conn, self, variant, name) + } + /// single thread only: retrieve then create pub fn get_or_create_symbol( &self, @@ -291,6 +313,7 @@ impl SymbolDependency { #[derive(Debug)] pub struct Translation { pub id: usize, + pub project_id: usize, pub key: String, pub value: String, } @@ -299,9 +322,10 @@ impl Model for Translation { fn table() -> String { " translation ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT NOT NULL, - value TEXT NOT NULL + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER REFERENCES project(id) ON DELETE CASCADE, + key TEXT NOT NULL, + value TEXT NOT NULL ) " .to_string() @@ -312,10 +336,39 @@ impl Translation { pub fn from_row(row: &Row) -> rusqlite::Result { Ok(Self { id: row.get(0)?, - key: row.get(1)?, - value: row.get(2)?, + project_id: row.get(1)?, + key: row.get(2)?, + value: row.get(3)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create( + conn: &Connection, + project: &Project, + key: &str, + value: &str, + ) -> anyhow::Result { + conn.execute( + "INSERT INTO translation (project_id, key, value) VALUES (?1, ?2, ?3)", + params![project.id, key, value], + )?; + let translation = conn.query_row( + "SELECT * FROM translation WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(translation) + } + + pub fn retrieve(conn: &Connection, project: &Project, key: &str) -> anyhow::Result { + let translation = conn.query_row( + "SELECT * FROM translation WHERE (project_id, key) = (?1, ?2)", + params![project.id, key], + Self::from_row, + )?; + Ok(translation) + } } // Join Table @@ -347,6 +400,24 @@ impl TranslationUsage { symbol_id: row.get(2)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create( + conn: &Connection, + translation: &Translation, + symbol: &Symbol, + ) -> anyhow::Result { + conn.execute( + "INSERT INTO translation_usage (translation_id, symbol_id) VALUES (?1, ?2)", + params![translation.id, symbol.id], + )?; + let translation = conn.query_row( + "SELECT * FROM translation_usage WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(translation) + } } #[derive(Debug)] diff --git a/crates/dt_i18n/src/collect.rs b/crates/dt_i18n/src/collect.rs index 2381b7f..83170dc 100644 --- a/crates/dt_i18n/src/collect.rs +++ b/crates/dt_i18n/src/collect.rs @@ -17,9 +17,9 @@ impl I18nToSymbol { &mut self, module_path: &str, module_ast: &Module, - ) -> anyhow::Result<()> { + ) -> anyhow::Result>> { let i18n_usage = core::collect_translation(module_ast)?; - for (symbol, i18n_keys) in i18n_usage { + for (symbol, i18n_keys) in i18n_usage.iter() { for i18n_key in i18n_keys.iter() { if !self.table.contains_key(i18n_key) { self.table.insert(i18n_key.to_owned(), HashMap::new()); @@ -38,6 +38,6 @@ impl I18nToSymbol { .insert(symbol.to_owned()); } } - Ok(()) + Ok(i18n_usage) } } From e44c781c55f4d875cccfedf3210d86dda172d611 Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 01:07:59 +0800 Subject: [PATCH 4/9] feat: create records for route and route usage of each parsed module --- crates/cli/src/main.rs | 51 +++++++++++++++++++++++++++----- crates/dt_database/src/models.rs | 47 +++++++++++++++++++++++++---- crates/dt_route/src/lib.rs | 23 +++++++------- 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 797faf6..9d93a41 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -12,7 +12,7 @@ use dt_core::{ }, path_resolver::{PathResolver, ToCanonicalString}, portable::Portable, - route::SymbolToRoutes, + route::{Route, SymbolToRoutes}, scheduler::ParserCandidateScheduler, }; use std::{ @@ -397,6 +397,33 @@ impl Project { } Ok(()) } + + pub fn add_route_usage( + &self, + module: &models::Module, + route_usage: &Vec, + ) -> anyhow::Result<()> { + for Route { path, depend_on } in route_usage.iter() { + let route = self + .project + .add_route(&self.db.conn, path) + .context(format!("create route {} for project", path))?; + for symbol_name in depend_on.iter() { + let symbol = module + .get_symbol( + &self.db.conn, + models::SymbolVariant::LocalVariable, + &symbol_name, + ) + .context(format!( + "try to add route for symbol {}, but symbol doesn't exist", + symbol_name, + ))?; + models::RouteUsage::create(&self.db.conn, &route, &symbol)?; + } + } + Ok(()) + } } fn main() -> anyhow::Result<()> { @@ -423,17 +450,27 @@ fn main() -> anyhow::Result<()> { let module_ast = Input::Path(module_src).get_module_ast()?; let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; let i18n_usage = i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; - symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; + let route_usage = + symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; - let module = project.add_module(&symbol_dependency).context(format!( - "add module {} to project", - symbol_dependency.canonical_path - ))?; + let module = project + .add_module(&symbol_dependency) + .context(format!( + "add module {} to project", + symbol_dependency.canonical_path + )) + .context(format!("add module {} to project", module_src))?; project .add_i18n_usage(&module, &i18n_usage) .context(format!( "add i18n usage of module {} to project", - symbol_dependency.canonical_path + module_src + ))?; + project + .add_route_usage(&module, &route_usage) + .context(format!( + "add route usage of module {} to project", + module_src ))?; depend_on_graph.add_symbol_dependency(symbol_dependency)?; diff --git a/crates/dt_database/src/models.rs b/crates/dt_database/src/models.rs index 619dbae..36700e6 100644 --- a/crates/dt_database/src/models.rs +++ b/crates/dt_database/src/models.rs @@ -69,6 +69,10 @@ impl Project { pub fn get_translation(&self, conn: &Connection, key: &str) -> anyhow::Result { Translation::retrieve(conn, self, key) } + + pub fn add_route(&self, conn: &Connection, path: &str) -> anyhow::Result { + Route::create(conn, self, path) + } } #[derive(Debug)] @@ -423,6 +427,7 @@ impl TranslationUsage { #[derive(Debug)] pub struct Route { pub id: usize, + pub project_id: usize, pub path: String, } @@ -430,8 +435,9 @@ impl Model for Route { fn table() -> String { " route ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT NOT NULL + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER REFERENCES project(id) ON DELETE CASCADE, + path TEXT NOT NULL ) " .to_string() @@ -442,9 +448,24 @@ impl Route { pub fn from_row(row: &Row) -> rusqlite::Result { Ok(Self { id: row.get(0)?, - path: row.get(1)?, + project_id: row.get(1)?, + path: row.get(2)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create(conn: &Connection, project: &Project, path: &str) -> anyhow::Result { + conn.execute( + "INSERT INTO route (project_id, path) VALUES (?1, ?2)", + params![project.id, path], + )?; + let route = conn.query_row( + "SELECT * FROM route WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(route) + } } // Join Table @@ -459,9 +480,9 @@ impl Model for RouteUsage { fn table() -> String { " route_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - translation_id INTEGER REFERENCES route(id) ON DELETE CASCADE, - symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE + id INTEGER PRIMARY KEY AUTOINCREMENT, + route_id INTEGER REFERENCES route(id) ON DELETE CASCADE, + symbol_id INTEGER REFERENCES symbol(id) ON DELETE CASCADE ) " .to_string() @@ -476,4 +497,18 @@ impl RouteUsage { symbol_id: row.get(2)?, }) } + + /// single thread only: last_insert_rowid() + pub fn create(conn: &Connection, route: &Route, symbol: &Symbol) -> anyhow::Result { + conn.execute( + "INSERT INTO route_usage (route_id, symbol_id) VALUES (?1, ?2)", + params![route.id, symbol.id], + )?; + let route = conn.query_row( + "SELECT * FROM route_usage WHERE id=last_insert_rowid()", + (), + Self::from_row, + )?; + Ok(route) + } } diff --git a/crates/dt_route/src/lib.rs b/crates/dt_route/src/lib.rs index 3c0a0da..0978083 100644 --- a/crates/dt_route/src/lib.rs +++ b/crates/dt_route/src/lib.rs @@ -24,12 +24,13 @@ impl SymbolToRoutes { &mut self, module_ast: &Module, symbol_dependency: &SymbolDependency, - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { if Self::should_collect(symbol_dependency) { let routes = Self::collect(module_ast, symbol_dependency)?; - self.aggregate(symbol_dependency.canonical_path.as_str(), routes); + self.aggregate(symbol_dependency.canonical_path.as_str(), &routes); + return Ok(routes); } - Ok(()) + Ok(vec![]) } fn should_collect(symbol_dependency: &SymbolDependency) -> bool { @@ -71,14 +72,14 @@ impl SymbolToRoutes { Ok(route_visitor.routes) } - fn aggregate(&mut self, module_path: &str, routes: Vec) { + fn aggregate(&mut self, module_path: &str, routes: &Vec) { let mut map = HashMap::new(); for route in routes { - for symbol in route.depend_on { - if !map.contains_key(&symbol) { - map.insert(symbol, vec![route.path.to_owned()]); + for symbol in route.depend_on.iter() { + if !map.contains_key(symbol) { + map.insert(symbol.to_string(), vec![route.path.to_owned()]); } else { - map.get_mut(&symbol).unwrap().push(route.path.to_owned()); + map.get_mut(symbol).unwrap().push(route.path.to_owned()); } } } @@ -87,9 +88,9 @@ impl SymbolToRoutes { } #[derive(Debug)] -struct Route { - path: String, - depend_on: HashSet, +pub struct Route { + pub path: String, + pub depend_on: HashSet, } #[derive(Debug)] From 4994621cd49ce9b14d40a8691f89de5d39ef55ac Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 13:38:36 +0800 Subject: [PATCH 5/9] feat: use db to trace paths --- Cargo.lock | 1 + crates/api_server/src/main.rs | 98 ++++++++++++++++++++- crates/cli/src/main.rs | 8 +- crates/dt_database/src/lib.rs | 39 -------- crates/dt_database/src/models.rs | 125 +++++++++++++++++++++++++- crates/dt_tracker/Cargo.toml | 5 +- crates/dt_tracker/src/db_version.rs | 132 ++++++++++++++++++++++++++++ crates/dt_tracker/src/lib.rs | 2 + 8 files changed, 357 insertions(+), 53 deletions(-) create mode 100644 crates/dt_tracker/src/db_version.rs diff --git a/Cargo.lock b/Cargo.lock index 168b4c4..9cc0d6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1098,6 +1098,7 @@ name = "dt_tracker" version = "0.1.0" dependencies = [ "anyhow", + "dt_database", "dt_graph", "dt_parser", "serde", diff --git a/crates/api_server/src/main.rs b/crates/api_server/src/main.rs index 492c287..c534780 100644 --- a/crates/api_server/src/main.rs +++ b/crates/api_server/src/main.rs @@ -2,9 +2,12 @@ use actix_cors::Cors; use actix_web::{error, get, web, App, HttpServer, Result}; use clap::Parser; use dt_core::{ + database::{models, Database, SqliteDb}, graph::used_by_graph::UsedByGraph, portable::Portable, - tracker::{DependencyTracker, TraceTarget}, + tracker::{ + db_version::DependencyTracker as DependencyTrackerV2, DependencyTracker, TraceTarget, + }, }; use serde::{Deserialize, Serialize}; use std::{ @@ -39,8 +42,8 @@ struct Info { exact_match: bool, } -#[get("/search")] -async fn search( +#[get("/search/in-memory")] +async fn search_in_memory( data: web::Data, info: web::Query, ) -> Result> { @@ -141,6 +144,92 @@ async fn search( })) } +#[get("/search/db")] +async fn search_db(info: web::Query) -> Result> { + let search = &info.q; + let exact_match = info.exact_match; + // TODO: share the db connection + let db = SqliteDb::open("./database/1010.db3").unwrap(); + let project = models::Project::retrieve_by_name(&db.conn, "kirby").unwrap(); + let matched_i18n_keys = project + .search_translation(&db.conn, search, exact_match) + .unwrap(); + if matched_i18n_keys.len() == 0 { + return Err(error::ErrorNotFound(format!("No result for {}", search))); + } + let mut dependency_tracker = DependencyTrackerV2::new(&db, project.clone(), true); + let mut trace_result = HashMap::new(); + for translation in matched_i18n_keys.iter() { + let mut route_to_paths = HashMap::new(); + let translation_used_by = translation.get_used_by(&db.conn).unwrap(); + for symbol in translation_used_by.iter() { + let module = models::Module::retrieve_by_id(&db.conn, symbol.module_id).unwrap(); + let full_paths = dependency_tracker + .trace(( + module.path.to_string(), + TraceTarget::LocalVar(symbol.name.to_string()), + )) + .unwrap(); + // traverse each path and check if any symbol is used in some routes + for mut full_path in full_paths { + full_path.reverse(); + for (i, (step_module_path, step_trace_target)) in full_path.iter().enumerate() { + match step_trace_target { + TraceTarget::LocalVar(step_symbol_name) => { + let step_module = + project.get_module(&db.conn, &step_module_path).unwrap(); + let step_symbol = step_module + .get_symbol( + &db.conn, + models::SymbolVariant::LocalVariable, + &step_symbol_name, + ) + .unwrap(); + let routes = step_symbol.get_used_by_routes(&db.conn).unwrap(); + if routes.len() > 0 { + let dependency_from_target_to_route: Vec = full_path[0..i] + .iter() + .map(|(path, target)| Step { + module_path: path.clone(), + symbol_name: target.to_string(), + }) + .collect(); + for route in routes.iter() { + let route = &route.path; + let symbol = &symbol.name; + if !route_to_paths.contains_key(route) { + route_to_paths.insert(route.clone(), HashMap::new()); + } + if !route_to_paths.get(route).unwrap().contains_key(symbol) { + route_to_paths + .get_mut(route) + .unwrap() + .insert(symbol.to_string(), vec![]); + } + route_to_paths + .get_mut(route) + .unwrap() + .get_mut(symbol) + .unwrap() + .push(dependency_from_target_to_route.clone()); + } + } + } + _ => (), + } + } + } + } + + trace_result.insert(translation.key.to_string(), route_to_paths); + } + + Ok(web::Json(SearchResponse { + project_root: "".to_string(), + trace_result, + })) +} + #[derive(Parser)] #[command(version, about = "Start the server to provide search API", long_about = None)] struct Cli { @@ -167,7 +256,8 @@ async fn main() -> std::io::Result<()> { symbol_to_route: portable.symbol_to_route.clone(), used_by_graph: portable.used_by_graph.clone(), })) - .service(search) + .service(search_in_memory) + .service(search_db) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 9d93a41..322666b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -46,10 +46,10 @@ struct Project { } impl Project { - pub fn new(project_root: &str, db_path: &str) -> anyhow::Result { + pub fn new(project_name: &str, project_root: &str, db_path: &str) -> anyhow::Result { let db = SqliteDb::open(db_path)?; db.create_tables()?; - let project = models::Project::create(&db.conn, project_root)?; + let project = models::Project::create(&db.conn, project_root, project_name)?; Ok(Self { db, project_root: project_root.to_owned(), @@ -345,7 +345,7 @@ impl Project { &self, symbol_dependency: &SymbolDependency, ) -> anyhow::Result { - let module = self.project.add_module( + let module = self.project.get_or_create_module( &self.db.conn, &self.remove_prefix(&symbol_dependency.canonical_path), )?; @@ -429,7 +429,7 @@ impl Project { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let project_root = PathBuf::from(&cli.input).to_canonical_string()?; - let project = Project::new(&project_root, "./database/1010.db3")?; + let project = Project::new("kirby", &project_root, "./database/1010.db3")?; let translation_file = File::open(&cli.translation_path)?; let translation_json_reader = BufReader::new(translation_file); let mut scheduler = ParserCandidateScheduler::new(&project_root); diff --git a/crates/dt_database/src/lib.rs b/crates/dt_database/src/lib.rs index 4776223..4cbf12e 100644 --- a/crates/dt_database/src/lib.rs +++ b/crates/dt_database/src/lib.rs @@ -48,42 +48,3 @@ impl Database for SqliteDb { Ok(()) } } - -#[test] -fn db_works() { - let db = - SqliteDb::open("/Users/linweitang/rust/js-symbol-dependency-tracker/database/test.db3") - .unwrap(); - db.create_tables().unwrap(); - println!("{:#?}", db); - - for project in db - .conn - .prepare("SELECT * FROM project") - .unwrap() - .query_map([], Project::from_row) - .unwrap() - { - println!("{:?}", project.unwrap()); - } - - for module in db - .conn - .prepare("SELECT * FROM module") - .unwrap() - .query_map([], Module::from_row) - .unwrap() - { - println!("{:?}", module.unwrap()); - } - - for symbol in db - .conn - .prepare("SELECT * FROM symbol") - .unwrap() - .query_map([], Symbol::from_row) - .unwrap() - { - println!("{:?}", symbol.unwrap()); - } -} diff --git a/crates/dt_database/src/models.rs b/crates/dt_database/src/models.rs index 36700e6..aba62ce 100644 --- a/crates/dt_database/src/models.rs +++ b/crates/dt_database/src/models.rs @@ -4,10 +4,11 @@ pub trait Model { fn table() -> String; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Project { pub id: usize, pub path: String, + pub name: String, } impl Model for Project { @@ -15,7 +16,8 @@ impl Model for Project { " project ( id INTEGER PRIMARY KEY, - path TEXT NOT NULL + path TEXT NOT NULL, + name TEXT UNIQUE NOT NULL ) " .to_string() @@ -27,12 +29,16 @@ impl Project { Ok(Self { id: row.get(0)?, path: row.get(1)?, + name: row.get(2)?, }) } /// single thread only: last_insert_rowid() - pub fn create(conn: &Connection, path: &str) -> anyhow::Result { - conn.execute("INSERT INTO project (path) VALUES (?1)", params![path])?; + pub fn create(conn: &Connection, path: &str, name: &str) -> anyhow::Result { + conn.execute( + "INSERT INTO project (path, name) VALUES (?1, ?2)", + params![path, name], + )?; let project = conn.query_row( "SELECT * FROM project WHERE id=last_insert_rowid()", (), @@ -41,6 +47,15 @@ impl Project { Ok(project) } + pub fn retrieve_by_name(conn: &Connection, name: &str) -> anyhow::Result { + let project = conn.query_row( + "SELECT * FROM project WHERE (name) = (?1)", + params![name], + Self::from_row, + )?; + Ok(project) + } + pub fn add_module(&self, conn: &Connection, path: &str) -> anyhow::Result { Module::create(conn, self, path) } @@ -70,6 +85,18 @@ impl Project { Translation::retrieve(conn, self, key) } + pub fn search_translation( + &self, + conn: &Connection, + search: &str, + exact_match: bool, + ) -> anyhow::Result> { + match exact_match { + true => Translation::search_value_exact_match(conn, self, search), + false => Translation::search_value_contain(conn, self, search), + } + } + pub fn add_route(&self, conn: &Connection, path: &str) -> anyhow::Result { Route::create(conn, self, path) } @@ -118,6 +145,15 @@ impl Module { Ok(module) } + pub fn retrieve_by_id(conn: &Connection, module_id: usize) -> anyhow::Result { + let module = conn.query_row( + "SELECT * FROM module WHERE id = ?1", + params![module_id], + Self::from_row, + )?; + Ok(module) + } + pub fn retrieve(conn: &Connection, project: &Project, path: &str) -> anyhow::Result { let module = conn.query_row( "SELECT * FROM module WHERE (project_id, path) = (?1, ?2)", @@ -263,6 +299,38 @@ impl Symbol { )?; Ok(symbol) } + + pub fn get_used_by(&self, conn: &Connection) -> anyhow::Result> { + let used_by: Vec = conn + .prepare( + " + SELECT s.* + FROM symbol s + JOIN symbol_dependency sd ON s.id = sd.symbol_id + WHERE sd.depend_on_symbol_id = ?1; + ", + )? + .query_map(params![self.id], Symbol::from_row)? + .map(|s| s.unwrap()) + .collect(); + Ok(used_by) + } + + pub fn get_used_by_routes(&self, conn: &Connection) -> anyhow::Result> { + let used_by_routes: Vec = conn + .prepare( + " + SELECT r.* + FROM route r + JOIN route_usage ru ON r.id = ru.route_id + WHERE ru.symbol_id = ?1; + ", + )? + .query_map(params![self.id], Route::from_row)? + .map(|s| s.unwrap()) + .collect(); + Ok(used_by_routes) + } } // Join Table @@ -373,6 +441,55 @@ impl Translation { )?; Ok(translation) } + + pub fn search_value_exact_match( + conn: &Connection, + project: &Project, + search: &str, + ) -> anyhow::Result> { + let translations: Vec = conn + .prepare("SELECT * FROM translation WHERE (project_id, value) = (?1, ?2)")? + .query_map(params![project.id, search], Self::from_row)? + .map(|s| s.unwrap()) + .collect(); + Ok(translations) + } + + pub fn search_value_contain( + conn: &Connection, + project: &Project, + search: &str, + ) -> anyhow::Result> { + let translations: Vec = conn + .prepare( + " + SELECT * + FROM translation + WHERE project_id = ?1 + AND value LIKE '%' || ?2 || '%' + ", + )? + .query_map(params![project.id, search], Self::from_row)? + .map(|s| s.unwrap()) + .collect(); + Ok(translations) + } + + pub fn get_used_by(&self, conn: &Connection) -> anyhow::Result> { + let used_by: Vec = conn + .prepare( + " + SELECT s.* + FROM symbol s + JOIN translation_usage tu ON s.id = tu.symbol_id + WHERE tu.translation_id = ?1; + ", + )? + .query_map(params![self.id], Symbol::from_row)? + .map(|s| s.unwrap()) + .collect(); + Ok(used_by) + } } // Join Table diff --git a/crates/dt_tracker/Cargo.toml b/crates/dt_tracker/Cargo.toml index 4ef0f34..ef1730e 100644 --- a/crates/dt_tracker/Cargo.toml +++ b/crates/dt_tracker/Cargo.toml @@ -12,8 +12,9 @@ swc_core = { workspace = true } swc_ecma_parser = { workspace = true } serde = { workspace = true } -dt_graph = { version = "0.1.0", path = "../dt_graph" } -dt_parser = { version = "0.1.0", path = "../dt_parser" } +dt_database = { version = "0.1.0", path = "../dt_database" } +dt_graph = { version = "0.1.0", path = "../dt_graph" } +dt_parser = { version = "0.1.0", path = "../dt_parser" } [dev-dependencies] \ No newline at end of file diff --git a/crates/dt_tracker/src/db_version.rs b/crates/dt_tracker/src/db_version.rs new file mode 100644 index 0000000..d3c03cb --- /dev/null +++ b/crates/dt_tracker/src/db_version.rs @@ -0,0 +1,132 @@ +use crate::TraceTarget; + +use super::ModuleSymbol; +use anyhow::Context; +use dt_database::{models, SqliteDb}; +use std::collections::HashMap; + +pub struct DependencyTracker<'db> { + cache: HashMap>>, + db: &'db SqliteDb, + project: models::Project, + trace_full_path_only: bool, +} + +impl<'db> DependencyTracker<'db> { + pub fn new(db: &'db SqliteDb, project: models::Project, trace_full_path_only: bool) -> Self { + Self { + cache: HashMap::new(), + db, + project, + trace_full_path_only, + } + } + + // Current implementation is mimick version of the trace with in-memory graph. + // We can refactor it after the database feature gets validated. + pub fn trace(&mut self, module_symbol: ModuleSymbol) -> anyhow::Result>> { + // Treat routeNmaes specially since they cause a lot of circular dependencies in + // some of our codebases. One assumption of this tool is "no circular dependency" + // , so let's workaround here for now. + if module_symbol.1.to_string() == "routeNames" { + return Ok(vec![]); + } + + // early return if cached + if let Some(cached) = self.cache.get(&module_symbol) { + return Ok(cached.clone()); + } + + let module = self + .project + .get_module(&self.db.conn, &module_symbol.0) + .context(format!("module {} not found", module_symbol.0))?; + + let symbol = match &module_symbol.1 { + crate::TraceTarget::NamedExport(name) => module + .get_symbol(&self.db.conn, models::SymbolVariant::NamedExport, name) + .context(format!( + "module {} doesn't have named export symbol {}", + module.path, name + ))?, + crate::TraceTarget::DefaultExport => module + .get_symbol(&self.db.conn, models::SymbolVariant::DefaultExport, "") + .context(format!( + "module {} doesn't have default export symbol", + module.path + ))?, + crate::TraceTarget::LocalVar(name) => module + .get_symbol(&self.db.conn, models::SymbolVariant::LocalVariable, name) + .context(format!( + "module {} doesn't have local variable symbol {}", + module.path, name + ))?, + }; + + let used_by = symbol + .get_used_by(&self.db.conn) + .context(format!("get used-by vector for symbol {}", symbol.name))?; + + let mut res: Vec> = vec![]; + for next_target in used_by.iter() { + let mut paths = match next_target.module_id == symbol.module_id { + true => { + // used by symbol from the same module + match next_target.variant { + models::SymbolVariant::NamedExport => self.trace(( + module_symbol.0.clone(), + TraceTarget::NamedExport(next_target.name.to_string()), + ))?, + models::SymbolVariant::DefaultExport => { + self.trace((module_symbol.0.clone(), TraceTarget::DefaultExport))? + } + models::SymbolVariant::LocalVariable => self.trace(( + module_symbol.0.clone(), + TraceTarget::LocalVar(next_target.name.to_string()), + ))?, + } + } + false => { + // used by symbol from other module + let other_module = + models::Module::retrieve_by_id(&self.db.conn, next_target.module_id)?; + match next_target.variant { + models::SymbolVariant::NamedExport => self.trace(( + other_module.path.clone(), + TraceTarget::NamedExport(next_target.name.to_string()), + ))?, + models::SymbolVariant::DefaultExport => { + self.trace((other_module.path.clone(), TraceTarget::DefaultExport))? + } + models::SymbolVariant::LocalVariable => self.trace(( + other_module.path.clone(), + TraceTarget::LocalVar(next_target.name.to_string()), + ))?, + } + } + }; + res.append(&mut paths); + } + + // append current ModuleSymbol to each path + for path in res.iter_mut() { + path.push(module_symbol.clone()); + } + if self.trace_full_path_only { + // because we only want to trace the full path, we only need to add a new path + // when this ModuleSymbol is not using by anyone. + if res.len() == 0 { + res.push(vec![module_symbol.clone()]); + } + } else { + // always append the current ModuleSymbol since we want to list every single path + // that is reachable from the target. + res.push(vec![module_symbol.clone()]); + } + + // update cache + self.cache.insert(module_symbol.clone(), res.clone()); + + Ok(res) + } +} diff --git a/crates/dt_tracker/src/lib.rs b/crates/dt_tracker/src/lib.rs index 474e1b4..d537a4c 100644 --- a/crates/dt_tracker/src/lib.rs +++ b/crates/dt_tracker/src/lib.rs @@ -1,3 +1,5 @@ +pub mod db_version; + use anyhow::{bail, Context}; use dt_graph::used_by_graph::{UsedBy, UsedByGraph, UsedByOther, UsedByType}; use serde::{Deserialize, Serialize}; From fb2fb4773432be4a9529499820b356d55fdc660a Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 14:37:07 +0800 Subject: [PATCH 6/9] feat: replace in memory searching with database searching --- crates/api_server/src/main.rs | 154 ++++--------------------------- crates/cli/src/main.rs | 164 +++++++++++++++++++--------------- crates/dt_i18n/src/collect.rs | 4 +- crates/dt_i18n/src/lib.rs | 1 + crates/dt_route/src/lib.rs | 98 +++++++++++--------- 5 files changed, 164 insertions(+), 257 deletions(-) diff --git a/crates/api_server/src/main.rs b/crates/api_server/src/main.rs index c534780..7be3b51 100644 --- a/crates/api_server/src/main.rs +++ b/crates/api_server/src/main.rs @@ -3,26 +3,10 @@ use actix_web::{error, get, web, App, HttpServer, Result}; use clap::Parser; use dt_core::{ database::{models, Database, SqliteDb}, - graph::used_by_graph::UsedByGraph, - portable::Portable, - tracker::{ - db_version::DependencyTracker as DependencyTrackerV2, DependencyTracker, TraceTarget, - }, + tracker::{db_version::DependencyTracker, TraceTarget}, }; use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - fs::File, - io::Read, -}; - -struct AppState { - project_root: String, - translation_json: HashMap, - i18n_to_symbol: HashMap>>, - symbol_to_route: HashMap>>, - used_by_graph: UsedByGraph, -} +use std::collections::HashMap; #[derive(Serialize, Clone)] struct Step { @@ -42,122 +26,23 @@ struct Info { exact_match: bool, } -#[get("/search/in-memory")] -async fn search_in_memory( +#[get("/search")] +async fn search( data: web::Data, info: web::Query, ) -> Result> { + let db = &data.db; let search = &info.q; let exact_match = info.exact_match; - - let mut matched_i18n_keys: Vec = Vec::new(); - match exact_match { - true => { - for (i18n_key, translation) in data.translation_json.iter() { - if translation == search { - matched_i18n_keys.push(i18n_key.to_owned()); - } - } - } - false => { - for (i18n_key, translation) in data.translation_json.iter() { - if translation.contains(search) { - matched_i18n_keys.push(i18n_key.to_owned()); - } - } - } - } - - if matched_i18n_keys.len() == 0 { - return Err(error::ErrorNotFound(format!("No result for {}", search))); - } - - let mut dependency_tracker = DependencyTracker::new(&data.used_by_graph, true); - let mut trace_result = HashMap::new(); - for i18n_key in matched_i18n_keys.iter() { - let mut route_to_paths = HashMap::new(); - if let Some(i18n_key_usage) = data.i18n_to_symbol.get(i18n_key) { - for (module_path, symbols) in i18n_key_usage { - for symbol in symbols { - let full_paths = dependency_tracker - .trace((module_path.clone(), TraceTarget::LocalVar(symbol.clone()))) - .unwrap(); - // traverse each path and check if any symbol is used in some routes - for mut full_path in full_paths { - full_path.reverse(); - for (i, (step_module_path, step_trace_target)) in - full_path.iter().enumerate() - { - match step_trace_target { - TraceTarget::LocalVar(step_symbol_name) => { - if let Some(symbol_to_routes) = - data.symbol_to_route.get(step_module_path) - { - if let Some(routes) = symbol_to_routes.get(step_symbol_name) - { - let dependency_from_target_to_route: Vec = - full_path[0..i] - .iter() - .map(|(path, target)| Step { - module_path: path.clone(), - symbol_name: target.to_string(), - }) - .collect(); - for route in routes.iter() { - if !route_to_paths.contains_key(route) { - route_to_paths - .insert(route.clone(), HashMap::new()); - } - if !route_to_paths - .get(route) - .unwrap() - .contains_key(symbol) - { - route_to_paths - .get_mut(route) - .unwrap() - .insert(symbol.to_string(), vec![]); - } - route_to_paths - .get_mut(route) - .unwrap() - .get_mut(symbol) - .unwrap() - .push(dependency_from_target_to_route.clone()); - } - } - } - } - _ => (), - } - } - } - } - } - } - trace_result.insert(i18n_key.to_string(), route_to_paths); - } - - Ok(web::Json(SearchResponse { - project_root: data.project_root.to_owned(), - trace_result, - })) -} - -#[get("/search/db")] -async fn search_db(info: web::Query) -> Result> { - let search = &info.q; - let exact_match = info.exact_match; - // TODO: share the db connection - let db = SqliteDb::open("./database/1010.db3").unwrap(); - let project = models::Project::retrieve_by_name(&db.conn, "kirby").unwrap(); + // project name "default_project" can be different in feature "cross-project tracing" + let project = models::Project::retrieve_by_name(&db.conn, "default_project").unwrap(); let matched_i18n_keys = project .search_translation(&db.conn, search, exact_match) .unwrap(); if matched_i18n_keys.len() == 0 { return Err(error::ErrorNotFound(format!("No result for {}", search))); } - let mut dependency_tracker = DependencyTrackerV2::new(&db, project.clone(), true); + let mut dependency_tracker = DependencyTracker::new(&db, project.clone(), true); let mut trace_result = HashMap::new(); for translation in matched_i18n_keys.iter() { let mut route_to_paths = HashMap::new(); @@ -230,34 +115,29 @@ async fn search_db(info: web::Query) -> Result> })) } +struct AppState { + db: SqliteDb, +} + #[derive(Parser)] #[command(version, about = "Start the server to provide search API", long_about = None)] struct Cli { - /// Portable path - #[arg(short)] - portable: String, + /// The path of your database + #[arg(long)] + db: String, } #[actix_web::main] async fn main() -> std::io::Result<()> { let cli = Cli::parse(); - let mut file = File::open(cli.portable)?; - let mut exported = String::new(); - file.read_to_string(&mut exported)?; - let portable = Portable::import(&exported).unwrap(); HttpServer::new(move || { App::new() .wrap(Cors::default().allow_any_method().allow_any_origin()) .app_data(web::Data::new(AppState { - project_root: portable.project_root.clone(), - translation_json: portable.translation_json.clone(), - i18n_to_symbol: portable.i18n_to_symbol.clone(), - symbol_to_route: portable.symbol_to_route.clone(), - used_by_graph: portable.used_by_graph.clone(), + db: SqliteDb::open(&cli.db).expect(&format!("open database from {}", cli.db)), })) - .service(search_in_memory) - .service(search_db) + .service(search) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 322666b..e2b1ce9 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,8 +2,7 @@ use anyhow::Context; use clap::Parser; use dt_core::{ database::{models, Database, SqliteDb}, - graph::{depend_on_graph::DependOnGraph, used_by_graph::UsedByGraph}, - i18n::I18nToSymbol, + i18n::collect_translation, parser::{ anonymous_default_export::SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT, collect_symbol_dependency, @@ -11,14 +10,13 @@ use dt_core::{ Input, }, path_resolver::{PathResolver, ToCanonicalString}, - portable::Portable, - route::{Route, SymbolToRoutes}, + route::{collect_route_dependency, Route}, scheduler::ParserCandidateScheduler, }; use std::{ collections::{HashMap, HashSet}, fs::File, - io::{prelude::*, BufReader}, + io::BufReader, path::PathBuf, }; @@ -38,6 +36,93 @@ struct Cli { output: String, } +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + parse_export_project_to_database(&cli.input, &cli.output, &cli.translation_path) + .context("parse and export project to database")?; + + Ok(()) +} + +fn parse_export_project_to_database( + project_root: &str, + output_database_path: &str, + translation_file_path: &str, +) -> anyhow::Result<()> { + let project_root = PathBuf::from(project_root).to_canonical_string()?; + // project name "default_project" can be different in feature "cross-project tracing" + let project = + Project::new("default_project", &project_root, output_database_path).context(format!( + "ready to a emit the project to database, project: {}, database: {}", + project_root, output_database_path + ))?; + + let translation_file = File::open(translation_file_path).context(format!( + "open translation file, path: {}", + translation_file_path + ))?; + let translation_json_reader = BufReader::new(translation_file); + let translation_json: HashMap = + serde_json::from_reader(translation_json_reader).context(format!( + "deserialize translation file, path: {}", + translation_file_path + ))?; + project + .add_translation(&translation_json) + .context("add translation to project")?; + + let mut scheduler = ParserCandidateScheduler::new(&project_root); + loop { + match scheduler.get_one_candidate() { + Some(c) => { + let module_src = c + .to_str() + .context(format!("get module_src, path_buf: {:?}", c))?; + let module_ast = Input::Path(module_src) + .get_module_ast() + .context(format!("get module ast, module_src: {}", module_src))?; + + let symbol_dependency = collect_symbol_dependency(&module_ast, module_src) + .context(format!( + "collect symbol dependency for module: {}", + &module_src + ))?; + let module = project + .add_module(&symbol_dependency) + .context(format!( + "add symbol dependency of module {} to project", + symbol_dependency.canonical_path + )) + .context(format!("add module {} to project", module_src))?; + + let i18n_usage = collect_translation(&module_ast) + .context(format!("collect i18n usage for module: {}", &module_src))?; + project + .add_i18n_usage(&module, &i18n_usage) + .context(format!( + "add i18n usage of module {} to project", + module_src + ))?; + + let route_usage = collect_route_dependency(&module_ast, &symbol_dependency) + .context(format!("collect route usage for module: {}", &module_src))?; + project + .add_route_usage(&module, &route_usage) + .context(format!( + "add route usage of module {} to project", + module_src + ))?; + + scheduler.mark_candidate_as_parsed(c); + } + None => break, + } + } + + Ok(()) +} + struct Project { db: SqliteDb, project_root: String, @@ -425,72 +510,3 @@ impl Project { Ok(()) } } - -fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - let project_root = PathBuf::from(&cli.input).to_canonical_string()?; - let project = Project::new("kirby", &project_root, "./database/1010.db3")?; - let translation_file = File::open(&cli.translation_path)?; - let translation_json_reader = BufReader::new(translation_file); - let mut scheduler = ParserCandidateScheduler::new(&project_root); - let mut depend_on_graph = DependOnGraph::new(&project_root); - let mut symbol_to_route = SymbolToRoutes::new(); - let mut i18n_to_symbol = I18nToSymbol::new(); - - let translation_json: HashMap = - serde_json::from_reader(translation_json_reader)?; - project - .add_translation(&translation_json) - .context("add translation to project")?; - - loop { - match scheduler.get_one_candidate() { - Some(c) => { - let module_src = c.to_str().context(format!("to_str() failed: {:?}", c))?; - let module_ast = Input::Path(module_src).get_module_ast()?; - let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; - let i18n_usage = i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; - let route_usage = - symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; - - let module = project - .add_module(&symbol_dependency) - .context(format!( - "add module {} to project", - symbol_dependency.canonical_path - )) - .context(format!("add module {} to project", module_src))?; - project - .add_i18n_usage(&module, &i18n_usage) - .context(format!( - "add i18n usage of module {} to project", - module_src - ))?; - project - .add_route_usage(&module, &route_usage) - .context(format!( - "add route usage of module {} to project", - module_src - ))?; - - depend_on_graph.add_symbol_dependency(symbol_dependency)?; - scheduler.mark_candidate_as_parsed(c); - } - None => break, - } - } - - let portable = Portable::new( - project_root.to_owned(), - translation_json, - i18n_to_symbol.table, - symbol_to_route.table, - UsedByGraph::from(&depend_on_graph), - ); - - let serialized = portable.export()?; - let mut file = File::create(&cli.output)?; - file.write_all(serialized.as_bytes())?; - - Ok(()) -} diff --git a/crates/dt_i18n/src/collect.rs b/crates/dt_i18n/src/collect.rs index 83170dc..2459983 100644 --- a/crates/dt_i18n/src/collect.rs +++ b/crates/dt_i18n/src/collect.rs @@ -17,7 +17,7 @@ impl I18nToSymbol { &mut self, module_path: &str, module_ast: &Module, - ) -> anyhow::Result>> { + ) -> anyhow::Result<()> { let i18n_usage = core::collect_translation(module_ast)?; for (symbol, i18n_keys) in i18n_usage.iter() { for i18n_key in i18n_keys.iter() { @@ -38,6 +38,6 @@ impl I18nToSymbol { .insert(symbol.to_owned()); } } - Ok(i18n_usage) + Ok(()) } } diff --git a/crates/dt_i18n/src/lib.rs b/crates/dt_i18n/src/lib.rs index 5437391..1a5a639 100644 --- a/crates/dt_i18n/src/lib.rs +++ b/crates/dt_i18n/src/lib.rs @@ -3,3 +3,4 @@ mod collect; mod core; pub use collect::I18nToSymbol; +pub use core::collect_translation; diff --git a/crates/dt_route/src/lib.rs b/crates/dt_route/src/lib.rs index 0978083..589a71e 100644 --- a/crates/dt_route/src/lib.rs +++ b/crates/dt_route/src/lib.rs @@ -7,6 +7,56 @@ use swc_core::ecma::{ visit::{Visit, VisitWith}, }; +fn should_collect(symbol_dependency: &SymbolDependency) -> bool { + // filename should be "routes.js" + if !symbol_dependency.canonical_path.ends_with("/routes.js") { + return false; + } + // routes.js should have default export + if symbol_dependency.default_export.is_none() { + return false; + } + // routes.js should have anonumous default export + match symbol_dependency.default_export.as_ref().unwrap() { + dt_parser::types::ModuleExport::Local(exported_symbol) => { + if exported_symbol != SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT { + return false; + } + } + dt_parser::types::ModuleExport::ReExportFrom(_) => return false, + } + // default export should depend on some symbols + match symbol_dependency + .local_variable_table + .get(SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT) + .unwrap() + .depend_on + { + Some(_) => return true, + None => return false, + } +} + +fn collect( + module_ast: &Module, + symbol_dependency: &SymbolDependency, +) -> anyhow::Result> { + let mut route_visitor = RouteVisitor::new(symbol_dependency); + module_ast.visit_with(&mut route_visitor); + Ok(route_visitor.routes) +} + +pub fn collect_route_dependency( + module_ast: &Module, + symbol_dependency: &SymbolDependency, +) -> anyhow::Result> { + if should_collect(symbol_dependency) { + let routes = collect(module_ast, symbol_dependency)?; + return Ok(routes); + } + Ok(vec![]) +} + #[derive(Debug)] pub struct SymbolToRoutes { // one symbol can be used in multiple routes @@ -24,52 +74,12 @@ impl SymbolToRoutes { &mut self, module_ast: &Module, symbol_dependency: &SymbolDependency, - ) -> anyhow::Result> { - if Self::should_collect(symbol_dependency) { - let routes = Self::collect(module_ast, symbol_dependency)?; + ) -> anyhow::Result<()> { + if should_collect(symbol_dependency) { + let routes = collect(module_ast, symbol_dependency)?; self.aggregate(symbol_dependency.canonical_path.as_str(), &routes); - return Ok(routes); } - Ok(vec![]) - } - - fn should_collect(symbol_dependency: &SymbolDependency) -> bool { - // filename should be "routes.js" - if !symbol_dependency.canonical_path.ends_with("/routes.js") { - return false; - } - // routes.js should have default export - if symbol_dependency.default_export.is_none() { - return false; - } - // routes.js should have anonumous default export - match symbol_dependency.default_export.as_ref().unwrap() { - dt_parser::types::ModuleExport::Local(exported_symbol) => { - if exported_symbol != SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT { - return false; - } - } - dt_parser::types::ModuleExport::ReExportFrom(_) => return false, - } - // default export should depend on some symbols - match symbol_dependency - .local_variable_table - .get(SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT) - .unwrap() - .depend_on - { - Some(_) => return true, - None => return false, - } - } - - fn collect( - module_ast: &Module, - symbol_dependency: &SymbolDependency, - ) -> anyhow::Result> { - let mut route_visitor = RouteVisitor::new(symbol_dependency); - module_ast.visit_with(&mut route_visitor); - Ok(route_visitor.routes) + Ok(()) } fn aggregate(&mut self, module_path: &str, routes: &Vec) { From efd3b65bc0ff05f81fb90fae6bce9b2d4eb63cbc Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 15:31:05 +0800 Subject: [PATCH 7/9] docs: add sub-command for cli and update readme --- README.md | 60 +++++++++++++++++++-- assets/erd.jpeg | Bin 0 -> 49186 bytes crates/cli/src/main.rs | 117 +++++++++++++++++++++++++++++++++++------ 3 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 assets/erd.jpeg diff --git a/README.md b/README.md index 7a1f8c7..a1d04ca 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ flowchart TD reexport all the library crates: +- database - graph - i18n - parser @@ -88,6 +89,12 @@ reexport all the library crates: - scheduler - tracker +### Database + +`Database` defines the models using in the `cli` and `api_server` crate. + +![ERD](./assets/erd.jpeg) + ### Graph `DependOnGraph` takes the `SymbolDependency` one by one to construct a DAG. You have to add the `SymbolDependency` by topological order so that `DependOnGraph` can handle the wildcard import and export for you. @@ -203,11 +210,58 @@ let paths = dt.trace("", TraceTarget::LocalVar("variable_name")).un ### Demo -See the `demo` crate. You can run `cargo run --bin demo -- -s ./test-project/everybodyyyy -d ~/tmp`. +See the `demo` crate. -### Portable +``` +Track fine-grained symbol dependency graph + +Usage: demo -s -d + +Options: + -s Path of project to trace + -d Path of the output folder + -h, --help Print help + -V, --version Print version +``` + +### CLI + +See the `cli` crate. + +``` +Parse a project and serialize its output -See the `cli` crate. You can run `cargo run --bin cli -- -i -o `. +Usage: cli + +Commands: + portable Parse and export the project in portable format + database Parse and export the project in database format + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version +``` + +Usage: + +- `cli portable -i -t -o ` +- `cli database -i -t -o ` + +### API Server + +see the `api_server` crate. The database is the one generated by CLI with `database` command. + +``` +Start the server to provide search API + +Usage: api_server --db + +Options: + --db The path of your database + -h, --help Print help + -V, --version Print version +``` ## Client diff --git a/assets/erd.jpeg b/assets/erd.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..52b16fe04055a343752e3bc2307375f1d5a6bd5c GIT binary patch literal 49186 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 U`hgji4M=HVlsS|`fbsuL05_vm4FCWD literal 0 HcmV?d00001 diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e2b1ce9..c3d419a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,8 +1,9 @@ use anyhow::Context; -use clap::Parser; +use clap::{Parser, Subcommand}; use dt_core::{ database::{models, Database, SqliteDb}, - i18n::collect_translation, + graph::{depend_on_graph::DependOnGraph, used_by_graph::UsedByGraph}, + i18n::{collect_translation, I18nToSymbol}, parser::{ anonymous_default_export::SYMBOL_NAME_FOR_ANONYMOUS_DEFAULT_EXPORT, collect_symbol_dependency, @@ -10,42 +11,124 @@ use dt_core::{ Input, }, path_resolver::{PathResolver, ToCanonicalString}, - route::{collect_route_dependency, Route}, + portable::Portable, + route::{collect_route_dependency, Route, SymbolToRoutes}, scheduler::ParserCandidateScheduler, }; use std::{ collections::{HashMap, HashSet}, fs::File, - io::BufReader, + io::{BufReader, Write}, path::PathBuf, }; #[derive(Parser)] #[command(version, about = "Parse a project and serialize its output", long_about = None)] struct Cli { - /// Input path - #[arg(short)] - input: String, + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Parse and export the project in portable format + Portable { + /// Input path + #[arg(short)] + input: String, + + /// translation.json path + #[arg(short)] + translation_path: String, + + /// Output path + #[arg(short)] + output: String, + }, + + /// Parse and export the project in database format + Database { + /// Input path + #[arg(short)] + input: String, - /// translation.json path - #[arg(short)] - translation_path: String, + /// translation.json path + #[arg(short)] + translation_path: String, - /// Output path - #[arg(short)] - output: String, + /// Output path + #[arg(short)] + output: String, + }, } fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); + match Cli::parse().command { + Command::Portable { + input, + translation_path, + output, + } => { + parse_and_export_project_to_portable(&input, &output, &translation_path) + .context("parse and export project to portable")?; + } + Command::Database { + input, + translation_path, + output, + } => { + parse_and_export_project_to_database(&input, &output, &translation_path) + .context("parse and export project to database")?; + } + } + Ok(()) +} + +fn parse_and_export_project_to_portable( + project_root: &str, + output_portable_path: &str, + translation_file_path: &str, +) -> anyhow::Result<()> { + let project_root = PathBuf::from(project_root).to_canonical_string()?; + let translation_json = File::open(&translation_file_path)?; + let translation_json_reader = BufReader::new(translation_json); + + let mut scheduler = ParserCandidateScheduler::new(&project_root); + let mut depend_on_graph = DependOnGraph::new(&project_root); + let mut symbol_to_route = SymbolToRoutes::new(); + let mut i18n_to_symbol = I18nToSymbol::new(); + loop { + match scheduler.get_one_candidate() { + Some(c) => { + let module_src = c.to_str().context(format!("to_str() failed: {:?}", c))?; + let module_ast = Input::Path(module_src).get_module_ast()?; + let symbol_dependency = collect_symbol_dependency(&module_ast, module_src)?; + i18n_to_symbol.collect_i18n_usage(module_src, &module_ast)?; + symbol_to_route.collect_route_dependency(&module_ast, &symbol_dependency)?; + + depend_on_graph.add_symbol_dependency(symbol_dependency)?; + scheduler.mark_candidate_as_parsed(c); + } + None => break, + } + } + + let portable = Portable::new( + project_root.to_owned(), + serde_json::from_reader(translation_json_reader)?, + i18n_to_symbol.table, + symbol_to_route.table, + UsedByGraph::from(&depend_on_graph), + ); - parse_export_project_to_database(&cli.input, &cli.output, &cli.translation_path) - .context("parse and export project to database")?; + let serialized = portable.export()?; + let mut file = File::create(&output_portable_path)?; + file.write_all(serialized.as_bytes())?; Ok(()) } -fn parse_export_project_to_database( +fn parse_and_export_project_to_database( project_root: &str, output_database_path: &str, translation_file_path: &str, From 06cc8951927023559d75cd41aa36931f7de0ca5d Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 17:18:58 +0800 Subject: [PATCH 8/9] feat: log missing translation instead of panic --- crates/cli/src/main.rs | 20 ++++++++++++-------- web/src/search/components/TreeView.tsx | 15 +++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c3d419a..0375a81 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -553,14 +553,18 @@ impl Project { symbol_name, ))?; for key in i18n_keys.iter() { - let translation = - self.project - .get_translation(&self.db.conn, key) - .context(format!( - "try to add translation for symbol {}, but translation {} doesn't exist", - symbol_name, key - ))?; - models::TranslationUsage::create(&self.db.conn, &translation, &symbol)?; + match self.project.get_translation(&self.db.conn, key) { + Ok(translation) => { + models::TranslationUsage::create(&self.db.conn, &translation, &symbol) + .context(format!( + "relate symbol {} to translation {}", + symbol_name, key + ))?; + } + Err(_) => { + println!("try to add translation for symbol {}, but translation {} doesn't exist", symbol_name, key); + } + } } } Ok(()) diff --git a/web/src/search/components/TreeView.tsx b/web/src/search/components/TreeView.tsx index b45a19d..e680c20 100644 --- a/web/src/search/components/TreeView.tsx +++ b/web/src/search/components/TreeView.tsx @@ -23,13 +23,15 @@ const StyledTreeView = styled(SimpleTreeView)({ }); const mapAllModuleSymbolToString = ( - project_root: string, tracePaths: ModuleSymbol[][] ): string[][] => { return tracePaths.map((tracePath) => tracePath.map(({ module_path, symbol_name }) => { - let shorterPath = module_path.slice(project_root.length); - return `${symbol_name}@${shorterPath}`; + if (module_path.startsWith("/")) { + return `${symbol_name}@${module_path.slice(1)}`; + } else { + return `${symbol_name}@${module_path}`; + } }) ); }; @@ -86,7 +88,7 @@ export const TreeView = React.memo(function TreeView({ return ( @@ -103,10 +105,7 @@ export const TreeView = React.memo(function TreeView({ {Object.entries(traceTargets).map(([traceTarget, paths]) => ( ))} From f83c981e602d9730e2f32651b9b7e15bd0935e18 Mon Sep 17 00:00:00 2001 From: wtlin1228 Date: Fri, 11 Oct 2024 18:40:18 +0800 Subject: [PATCH 9/9] chore: add progress bar for cli --- Cargo.lock | 1 + Cargo.toml | 1 + crates/api_server/src/main.rs | 2 ++ crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 14 ++++++++++++-- crates/demo/Cargo.toml | 6 +++--- crates/demo/src/main.rs | 10 ++++------ 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cc0d6b..e85623b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,7 @@ dependencies = [ "anyhow", "clap", "dt_core", + "indicatif", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 3abbc87..220bf13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ swc_core = { version = "0.104.2", features = ["common", "ecma_ast", "ecma swc_ecma_parser = { version = "0.150.0", features = ["typescript"] } clap = { version = "4.5", features = ["derive"] } rusqlite = { version = "0.32.1", features = ["bundled"] } +indicatif = "0.17.8" \ No newline at end of file diff --git a/crates/api_server/src/main.rs b/crates/api_server/src/main.rs index 7be3b51..5a886fa 100644 --- a/crates/api_server/src/main.rs +++ b/crates/api_server/src/main.rs @@ -26,6 +26,8 @@ struct Info { exact_match: bool, } +// Current implementation is mimick version of the trace with in-memory graph. +// We can refactor it after the database feature gets validated. #[get("/search")] async fn search( data: web::Data, diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index be5ddc4..28c009a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -10,5 +10,6 @@ version = "0.1.0" anyhow = { workspace = true } clap = { workspace = true } serde_json = { workspace = true } +indicatif = { workspace = true } dt_core = { version = "0.1.0", path = "../dt_core" } \ No newline at end of file diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 0375a81..b124bb2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -15,6 +15,7 @@ use dt_core::{ route::{collect_route_dependency, Route, SymbolToRoutes}, scheduler::ParserCandidateScheduler, }; +use indicatif::{ProgressBar, ProgressStyle}; use std::{ collections::{HashMap, HashSet}, fs::File, @@ -156,6 +157,13 @@ fn parse_and_export_project_to_database( .context("add translation to project")?; let mut scheduler = ParserCandidateScheduler::new(&project_root); + let bar = ProgressBar::new(scheduler.get_total_remaining_candidate_count() as u64); + bar.set_style( + ProgressStyle::with_template( + "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", + )? + .progress_chars("##-"), + ); loop { match scheduler.get_one_candidate() { Some(c) => { @@ -198,11 +206,12 @@ fn parse_and_export_project_to_database( ))?; scheduler.mark_candidate_as_parsed(c); + bar.inc(1); } None => break, } } - + bar.finish_with_message("all modules parsed 🌲"); Ok(()) } @@ -562,7 +571,8 @@ impl Project { ))?; } Err(_) => { - println!("try to add translation for symbol {}, but translation {} doesn't exist", symbol_name, key); + // you can uncomment this to debug + // println!("try to add translation for symbol {}, but translation {} doesn't exist", symbol_name, key); } } } diff --git a/crates/demo/Cargo.toml b/crates/demo/Cargo.toml index 288be90..67c7710 100644 --- a/crates/demo/Cargo.toml +++ b/crates/demo/Cargo.toml @@ -7,12 +7,12 @@ version = "0.1.0" [dependencies] -anyhow = { workspace = true } -clap = { workspace = true } +anyhow = { workspace = true } +clap = { workspace = true } +indicatif = { workspace = true } umya-spreadsheet = "1.2.3" dialoguer = { version = "0.11.0", features = ["history"] } -indicatif = "0.17.8" console = "0.15.8" rand = "0.8.5" diff --git a/crates/demo/src/main.rs b/crates/demo/src/main.rs index 6c7ef6c..971f4d8 100644 --- a/crates/demo/src/main.rs +++ b/crates/demo/src/main.rs @@ -1,13 +1,8 @@ use anyhow::Context; use clap::Parser; use console::style; -use dialoguer::{theme::ColorfulTheme, BasicHistory, Confirm, Input, Select}; -use indicatif::{ProgressBar, ProgressStyle}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use std::path::PathBuf; - use demo::spreadsheet::write_to_spreadsheet; +use dialoguer::{theme::ColorfulTheme, BasicHistory, Confirm, Input, Select}; use dt_core::{ graph::{depend_on_graph::DependOnGraph, used_by_graph::UsedByGraph}, parser::{collect_symbol_dependency, Input as ModuleInput}, @@ -15,6 +10,9 @@ use dt_core::{ scheduler::ParserCandidateScheduler, tracker::{DependencyTracker, TraceTarget}, }; +use indicatif::{ProgressBar, ProgressStyle}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use std::path::PathBuf; const SYMBOL_TYPE_SELECTIONS: [&str; 3] = ["Default Export", "Named Export", "Local Variable"];