Initial SFERA platform baseline
This commit is contained in:
Generated
+116
@@ -0,0 +1,116 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bsl-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"query-parser",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "query-parser"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic-engine"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/bsl-parser",
|
||||
"crates/query-parser",
|
||||
"crates/semantic-engine",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "bsl-parser"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
query-parser = { path = "../query-parser" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::keywords::{is_function_start, is_procedure_start};
|
||||
use crate::models::{ParsedCall, SourceRange};
|
||||
use crate::parser::{extract_name_from_header, routine_ended};
|
||||
|
||||
pub fn extract_calls(source: &str) -> Vec<ParsedCall> {
|
||||
let mut result = Vec::new();
|
||||
let mut current_routine: Option<String> = None;
|
||||
for (index, line) in source.lines().enumerate() {
|
||||
let line_no = index + 1;
|
||||
let trimmed = strip_inline_comment(line).trim();
|
||||
if is_procedure_start(trimmed) || is_function_start(trimmed) {
|
||||
current_routine = Some(extract_name_from_header(trimmed));
|
||||
continue;
|
||||
}
|
||||
if routine_ended(trimmed) {
|
||||
current_routine = None;
|
||||
continue;
|
||||
}
|
||||
let Some(caller) = current_routine.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(callee) = simple_call_name(trimmed).or_else(|| condition_call_name(trimmed)) {
|
||||
result.push(ParsedCall {
|
||||
caller: caller.clone(),
|
||||
callee,
|
||||
source_range: SourceRange {
|
||||
line_start: line_no,
|
||||
line_end: line_no,
|
||||
column_start: 1,
|
||||
column_end: line.len() + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn simple_call_name(line: &str) -> Option<String> {
|
||||
if !line.ends_with(';') || !line.contains('(') || !line.contains(')') || line.contains('.') {
|
||||
return None;
|
||||
}
|
||||
let call_expr = line.split('=').next_back().unwrap_or(line).trim();
|
||||
let name = call_expr.split('(').next()?.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(name.to_string())
|
||||
}
|
||||
|
||||
fn condition_call_name(line: &str) -> Option<String> {
|
||||
let lowered = line.to_lowercase();
|
||||
let is_condition = (lowered.starts_with("если ") && lowered.contains(" тогда"))
|
||||
|| (lowered.starts_with("if ") && lowered.contains(" then"));
|
||||
if !is_condition || line.contains('.') {
|
||||
return None;
|
||||
}
|
||||
let before_then = if let Some(index) = lowered.find(" тогда") {
|
||||
&line[..index]
|
||||
} else if let Some(index) = lowered.find(" then") {
|
||||
&line[..index]
|
||||
} else {
|
||||
line
|
||||
};
|
||||
let name_part = before_then.split('(').next()?.split_whitespace().last()?;
|
||||
if name_part.eq_ignore_ascii_case("Если") || name_part.eq_ignore_ascii_case("If") {
|
||||
return None;
|
||||
}
|
||||
Some(name_part.to_string())
|
||||
}
|
||||
|
||||
fn strip_inline_comment(line: &str) -> &str {
|
||||
line.split("//").next().unwrap_or(line)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
pub fn is_procedure_start(line: &str) -> bool {
|
||||
let value = line.trim_start().to_lowercase();
|
||||
value.starts_with("процедура ") || value.starts_with("procedure ")
|
||||
}
|
||||
|
||||
pub fn is_function_start(line: &str) -> bool {
|
||||
let value = line.trim_start().to_lowercase();
|
||||
value.starts_with("функция ") || value.starts_with("function ")
|
||||
}
|
||||
|
||||
pub fn is_procedure_end(line: &str) -> bool {
|
||||
let value = line.trim_start().to_lowercase();
|
||||
value.starts_with("конецпроцедуры") || value.starts_with("endprocedure")
|
||||
}
|
||||
|
||||
pub fn is_function_end(line: &str) -> bool {
|
||||
let value = line.trim_start().to_lowercase();
|
||||
value.starts_with("конецфункции") || value.starts_with("endfunction")
|
||||
}
|
||||
|
||||
pub fn has_export(line: &str) -> bool {
|
||||
let value = line.to_lowercase();
|
||||
value.contains(" экспорт") || value.contains(" export")
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
pub mod calls;
|
||||
pub mod keywords;
|
||||
pub mod models;
|
||||
pub mod parser;
|
||||
pub mod queries;
|
||||
pub mod writes;
|
||||
|
||||
use crate::calls::extract_calls;
|
||||
use crate::models::ParsedSemanticUnit;
|
||||
use crate::parser::extract_procedures;
|
||||
use crate::queries::extract_queries;
|
||||
use crate::writes::extract_writes;
|
||||
|
||||
pub fn parse_module(source_path: &str, source: &str) -> ParsedSemanticUnit {
|
||||
ParsedSemanticUnit {
|
||||
source_path: source_path.to_string(),
|
||||
procedures: extract_procedures(source),
|
||||
calls: extract_calls(source),
|
||||
queries: extract_queries(source),
|
||||
writes: extract_writes(source),
|
||||
diagnostics: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_module;
|
||||
|
||||
#[test]
|
||||
fn parses_procedure_calls_query_and_write() {
|
||||
let source = r#"
|
||||
Процедура Проведение()
|
||||
ПроверитьОстатки();
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ПроверитьОстатки()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
assert_eq!(unit.procedures.len(), 2);
|
||||
assert_eq!(unit.calls.len(), 1);
|
||||
assert_eq!(unit.queries.len(), 1);
|
||||
assert_eq!(unit.writes.len(), 1);
|
||||
assert_eq!(unit.calls[0].caller, "Проведение");
|
||||
assert_eq!(unit.calls[0].callee, "ПроверитьОстатки");
|
||||
assert_eq!(unit.writes[0].target, "ОстаткиТоваров");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_english_function_query_call_and_write() {
|
||||
let source = r#"
|
||||
Procedure Posting()
|
||||
CheckStock(); // inline comment
|
||||
Movements.StockBalance.Write();
|
||||
EndProcedure
|
||||
|
||||
Function CheckStock()
|
||||
Query = New Query;
|
||||
Query.Text =
|
||||
"SELECT
|
||||
Stock.Item
|
||||
FROM
|
||||
AccumulationRegister.StockBalance AS Stock";
|
||||
EndFunction
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.procedures.len(), 2);
|
||||
assert!(unit.procedures[1].is_function);
|
||||
assert_eq!(unit.calls.len(), 1);
|
||||
assert_eq!(unit.calls[0].callee, "CheckStock");
|
||||
assert_eq!(unit.queries.len(), 1);
|
||||
assert_eq!(
|
||||
unit.queries[0].tables,
|
||||
vec!["AccumulationRegister.StockBalance"]
|
||||
);
|
||||
assert!(!unit.queries[0].query_text.ends_with('"'));
|
||||
assert_eq!(unit.writes.len(), 1);
|
||||
assert_eq!(unit.writes[0].target, "StockBalance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_mixed_russian_and_english_keywords_in_one_module() {
|
||||
let source = r#"
|
||||
Procedure Posting()
|
||||
ПроверитьОстатки();
|
||||
Movements.StockBalance.Write();
|
||||
EndProcedure
|
||||
|
||||
Процедура ПроверитьОстатки()
|
||||
Query = New Query;
|
||||
Query.Text =
|
||||
"SELECT
|
||||
Stock.Item
|
||||
FROM
|
||||
AccumulationRegister.StockBalance AS Stock";
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.procedures.len(), 2);
|
||||
assert_eq!(unit.procedures[0].name, "Posting");
|
||||
assert_eq!(unit.procedures[1].name, "ПроверитьОстатки");
|
||||
assert_eq!(unit.calls.len(), 1);
|
||||
assert_eq!(unit.calls[0].caller, "Posting");
|
||||
assert_eq!(unit.calls[0].callee, "ПроверитьОстатки");
|
||||
assert_eq!(unit.queries.len(), 1);
|
||||
assert_eq!(
|
||||
unit.queries[0].tables,
|
||||
vec!["AccumulationRegister.StockBalance"]
|
||||
);
|
||||
assert_eq!(unit.writes.len(), 1);
|
||||
assert_eq!(unit.writes[0].target, "StockBalance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_inline_query_assignment_with_pipe_prefixed_lines() {
|
||||
let source = r#"
|
||||
Процедура ПолучитьТовары()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст = "ВЫБРАТЬ
|
||||
|Товары.Ссылка
|
||||
|ИЗ
|
||||
|Справочник.Номенклатура КАК Товары";
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.queries.len(), 1);
|
||||
assert_eq!(unit.queries[0].tables, vec!["Справочник.Номенклатура"]);
|
||||
assert!(unit.queries[0].query_text.starts_with("ВЫБРАТЬ"));
|
||||
assert!(!unit.queries[0].query_text.contains("|ИЗ"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_query_from_and_table_on_same_line() {
|
||||
let source = r#"
|
||||
Процедура ПолучитьКонтрагентов()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст = "ВЫБРАТЬ
|
||||
|Контрагенты.Ссылка
|
||||
|ИЗ Справочник.Контрагенты КАК Контрагенты";
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.queries.len(), 1);
|
||||
assert_eq!(unit.queries[0].tables, vec!["Справочник.Контрагенты"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_assignment_function_calls() {
|
||||
let source = r#"
|
||||
Процедура Проведение()
|
||||
МожноПроводить = ПроверитьОстатки();
|
||||
КонецПроцедуры
|
||||
|
||||
Функция ПроверитьОстатки()
|
||||
Возврат Истина;
|
||||
КонецФункции
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.calls.len(), 1);
|
||||
assert_eq!(unit.calls[0].caller, "Проведение");
|
||||
assert_eq!(unit.calls[0].callee, "ПроверитьОстатки");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_condition_function_calls() {
|
||||
let source = r#"
|
||||
Процедура Проведение()
|
||||
Если ПроверитьОстатки() Тогда
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецЕсли;
|
||||
КонецПроцедуры
|
||||
|
||||
Функция ПроверитьОстатки()
|
||||
Возврат Истина;
|
||||
КонецФункции
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.calls.len(), 1);
|
||||
assert_eq!(unit.calls[0].caller, "Проведение");
|
||||
assert_eq!(unit.calls[0].callee, "ПроверитьОстатки");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_join_tables_from_query_text() {
|
||||
let source = r#"
|
||||
Процедура ПолучитьЗаказы()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст = "ВЫБРАТЬ
|
||||
|Заказы.Ссылка,
|
||||
|Контрагенты.Наименование
|
||||
|ИЗ Документ.ЗаказПокупателя КАК Заказы
|
||||
|ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
|
||||
|ПО Заказы.Контрагент = Контрагенты.Ссылка";
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(
|
||||
unit.queries[0].tables,
|
||||
vec!["Документ.ЗаказПокупателя", "Справочник.Контрагенты"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_object_write_targets_from_create_assignments() {
|
||||
let source = r#"
|
||||
Процедура СоздатьНоменклатуру()
|
||||
Элемент = Справочники.Номенклатура.СоздатьЭлемент();
|
||||
Элемент.Записать();
|
||||
КонецПроцедуры
|
||||
|
||||
Procedure CreateOrder()
|
||||
Order = Documents.CustomerOrder.CreateDocument();
|
||||
Order.Write();
|
||||
EndProcedure
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.writes.len(), 2);
|
||||
assert_eq!(unit.writes[0].target, "Справочник.Номенклатура");
|
||||
assert_eq!(unit.writes[0].write_type, "OBJECT_WRITE");
|
||||
assert_eq!(unit.writes[1].target, "Документ.CustomerOrder");
|
||||
assert_eq!(unit.writes[1].write_type, "OBJECT_WRITE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_recordset_write_targets_from_create_assignments() {
|
||||
let source = r#"
|
||||
Процедура ЗаписатьЦены()
|
||||
Набор = РегистрыСведений.Цены.СоздатьНаборЗаписей();
|
||||
Набор.Записать();
|
||||
КонецПроцедуры
|
||||
|
||||
Procedure WriteBalances()
|
||||
Records = AccumulationRegisters.StockBalance.CreateRecordSet();
|
||||
Records.Write();
|
||||
EndProcedure
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.writes.len(), 2);
|
||||
assert_eq!(unit.writes[0].target, "РегистрСведений.Цены");
|
||||
assert_eq!(unit.writes[0].write_type, "REGISTER_WRITE");
|
||||
assert_eq!(unit.writes[1].target, "РегистрНакопления.StockBalance");
|
||||
assert_eq!(unit.writes[1].write_type, "REGISTER_WRITE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_calls_and_writes_inside_control_flow_blocks() {
|
||||
let source = r#"
|
||||
Процедура Проведение()
|
||||
Для Каждого Строка Из Товары Цикл
|
||||
ПроверитьСтроку(Строка);
|
||||
КонецЦикла;
|
||||
|
||||
Попытка
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
Исключение
|
||||
СообщитьОбОшибке();
|
||||
КонецПопытки;
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура ПроверитьСтроку(Строка)
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура СообщитьОбОшибке()
|
||||
КонецПроцедуры
|
||||
"#;
|
||||
let unit = parse_module("module.bsl", source);
|
||||
|
||||
assert_eq!(unit.calls.len(), 2);
|
||||
assert_eq!(unit.calls[0].callee, "ПроверитьСтроку");
|
||||
assert_eq!(unit.calls[1].callee, "СообщитьОбОшибке");
|
||||
assert_eq!(unit.writes.len(), 1);
|
||||
assert_eq!(unit.writes[0].target, "ОстаткиТоваров");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
|
||||
use bsl_parser::parse_module;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let source_path = args.get(1).map(String::as_str).unwrap_or("-");
|
||||
let source = match args.get(1) {
|
||||
Some(path) => fs::read_to_string(path).unwrap_or_else(|error| {
|
||||
eprintln!("failed to read {path}: {error}");
|
||||
std::process::exit(2);
|
||||
}),
|
||||
None => {
|
||||
let mut buffer = String::new();
|
||||
io::stdin()
|
||||
.read_to_string(&mut buffer)
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("failed to read stdin: {error}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
let unit = parse_module(source_path, &source);
|
||||
serde_json::to_writer_pretty(io::stdout(), &unit).unwrap_or_else(|error| {
|
||||
eprintln!("failed to serialize parse result: {error}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
println!();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct SourceRange {
|
||||
pub line_start: usize,
|
||||
pub line_end: usize,
|
||||
pub column_start: usize,
|
||||
pub column_end: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParsedProcedure {
|
||||
pub name: String,
|
||||
pub export: bool,
|
||||
pub is_function: bool,
|
||||
pub parameters: Vec<String>,
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParsedCall {
|
||||
pub caller: String,
|
||||
pub callee: String,
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParsedQuery {
|
||||
pub owner_procedure: String,
|
||||
pub query_text: String,
|
||||
pub tables: Vec<String>,
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParsedWrite {
|
||||
pub owner_procedure: String,
|
||||
pub target: String,
|
||||
pub write_type: String,
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParseDiagnostic {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub severity: String,
|
||||
pub source_range: Option<SourceRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParsedSemanticUnit {
|
||||
pub source_path: String,
|
||||
pub procedures: Vec<ParsedProcedure>,
|
||||
pub calls: Vec<ParsedCall>,
|
||||
pub queries: Vec<ParsedQuery>,
|
||||
pub writes: Vec<ParsedWrite>,
|
||||
pub diagnostics: Vec<ParseDiagnostic>,
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::keywords::{
|
||||
has_export, is_function_end, is_function_start, is_procedure_end, is_procedure_start,
|
||||
};
|
||||
use crate::models::{ParsedProcedure, SourceRange};
|
||||
|
||||
pub fn extract_procedures(source: &str) -> Vec<ParsedProcedure> {
|
||||
let mut result = Vec::new();
|
||||
let mut current_index: Option<usize> = None;
|
||||
for (index, line) in source.lines().enumerate() {
|
||||
let line_no = index + 1;
|
||||
if is_procedure_start(line) || is_function_start(line) {
|
||||
let is_function = is_function_start(line);
|
||||
let name = extract_routine_name(line, is_function);
|
||||
result.push(ParsedProcedure {
|
||||
name,
|
||||
export: has_export(line),
|
||||
is_function,
|
||||
parameters: extract_parameters(line),
|
||||
source_range: SourceRange {
|
||||
line_start: line_no,
|
||||
line_end: line_no,
|
||||
column_start: 1,
|
||||
column_end: line.len() + 1,
|
||||
},
|
||||
});
|
||||
current_index = Some(result.len() - 1);
|
||||
} else if routine_ended(line) {
|
||||
if let Some(procedure_index) = current_index.take() {
|
||||
result[procedure_index].source_range.line_end = line_no;
|
||||
result[procedure_index].source_range.column_end = line.len() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn routine_ended(line: &str) -> bool {
|
||||
is_procedure_end(line) || is_function_end(line)
|
||||
}
|
||||
|
||||
pub fn extract_name_from_header(line: &str) -> String {
|
||||
line.split_whitespace()
|
||||
.nth(1)
|
||||
.unwrap_or("")
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn extract_routine_name(line: &str, is_function: bool) -> String {
|
||||
let trimmed = line.trim();
|
||||
let prefixes = if is_function {
|
||||
vec!["Функция", "Function"]
|
||||
} else {
|
||||
vec!["Процедура", "Procedure"]
|
||||
};
|
||||
for prefix in prefixes {
|
||||
if trimmed.to_lowercase().starts_with(&prefix.to_lowercase()) {
|
||||
return trimmed[prefix.len()..]
|
||||
.trim()
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn extract_parameters(line: &str) -> Vec<String> {
|
||||
let Some(start) = line.find('(') else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(end) = line[start + 1..].find(')') else {
|
||||
return Vec::new();
|
||||
};
|
||||
let params = &line[start + 1..start + 1 + end];
|
||||
params
|
||||
.split(',')
|
||||
.map(|p| p.trim().to_string())
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::keywords::{is_function_start, is_procedure_start};
|
||||
use crate::models::{ParsedQuery, SourceRange};
|
||||
use crate::parser::{extract_name_from_header, routine_ended};
|
||||
use query_parser::extract_tables;
|
||||
|
||||
pub fn extract_queries(source: &str) -> Vec<ParsedQuery> {
|
||||
let mut result = Vec::new();
|
||||
let mut current_routine: Option<String> = None;
|
||||
let mut collecting = false;
|
||||
let mut query_lines: Vec<String> = Vec::new();
|
||||
let mut query_start_line = 0;
|
||||
|
||||
for (index, line) in source.lines().enumerate() {
|
||||
let line_no = index + 1;
|
||||
let trimmed = strip_inline_comment(line).trim();
|
||||
if is_procedure_start(trimmed) || is_function_start(trimmed) {
|
||||
current_routine = Some(extract_name_from_header(trimmed));
|
||||
continue;
|
||||
}
|
||||
if routine_ended(trimmed) {
|
||||
current_routine = None;
|
||||
collecting = false;
|
||||
query_lines.clear();
|
||||
continue;
|
||||
}
|
||||
if trimmed.contains(".Текст") || trimmed.to_lowercase().contains(".text") {
|
||||
collecting = true;
|
||||
query_start_line = line_no;
|
||||
if let Some(inline_query) = query_text_after_assignment(trimmed) {
|
||||
query_lines.push(clean_query_line(inline_query));
|
||||
if trimmed.ends_with(';') {
|
||||
push_query(
|
||||
&mut result,
|
||||
current_routine.as_ref(),
|
||||
&query_lines,
|
||||
query_start_line,
|
||||
line_no,
|
||||
line.len() + 1,
|
||||
);
|
||||
collecting = false;
|
||||
query_lines.clear();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if collecting {
|
||||
query_lines.push(clean_query_line(trimmed));
|
||||
if trimmed.ends_with(';') {
|
||||
push_query(
|
||||
&mut result,
|
||||
current_routine.as_ref(),
|
||||
&query_lines,
|
||||
query_start_line,
|
||||
line_no,
|
||||
line.len() + 1,
|
||||
);
|
||||
collecting = false;
|
||||
query_lines.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn push_query(
|
||||
result: &mut Vec<ParsedQuery>,
|
||||
owner: Option<&String>,
|
||||
query_lines: &[String],
|
||||
line_start: usize,
|
||||
line_end: usize,
|
||||
column_end: usize,
|
||||
) {
|
||||
let Some(owner) = owner else {
|
||||
return;
|
||||
};
|
||||
let query_text = query_lines
|
||||
.iter()
|
||||
.filter(|line| !line.is_empty())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let tables = extract_tables(&query_text);
|
||||
result.push(ParsedQuery {
|
||||
owner_procedure: owner.clone(),
|
||||
query_text,
|
||||
tables,
|
||||
source_range: SourceRange {
|
||||
line_start,
|
||||
line_end,
|
||||
column_start: 1,
|
||||
column_end,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn clean_query_line(line: &str) -> String {
|
||||
let mut value = line
|
||||
.trim()
|
||||
.trim_end_matches(';')
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim();
|
||||
if let Some(stripped) = value.strip_prefix('|') {
|
||||
value = stripped.trim();
|
||||
}
|
||||
value.trim_matches('"').trim().to_string()
|
||||
}
|
||||
|
||||
fn query_text_after_assignment(line: &str) -> Option<&str> {
|
||||
let (_, value) = line.split_once('=')?;
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_inline_comment(line: &str) -> &str {
|
||||
line.split("//").next().unwrap_or(line)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use crate::keywords::{is_function_start, is_procedure_start};
|
||||
use crate::models::{ParsedWrite, SourceRange};
|
||||
use crate::parser::{extract_name_from_header, routine_ended};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn extract_writes(source: &str) -> Vec<ParsedWrite> {
|
||||
let mut result = Vec::new();
|
||||
let mut current_routine: Option<String> = None;
|
||||
let mut object_targets: HashMap<String, String> = HashMap::new();
|
||||
for (index, line) in source.lines().enumerate() {
|
||||
let line_no = index + 1;
|
||||
let trimmed = strip_inline_comment(line).trim();
|
||||
if is_procedure_start(trimmed) || is_function_start(trimmed) {
|
||||
current_routine = Some(extract_name_from_header(trimmed));
|
||||
object_targets.clear();
|
||||
continue;
|
||||
}
|
||||
if routine_ended(trimmed) {
|
||||
current_routine = None;
|
||||
continue;
|
||||
}
|
||||
let Some(owner) = current_routine.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let lowered = trimmed.to_lowercase();
|
||||
if let Some((variable, target)) = extract_object_create_target(trimmed) {
|
||||
object_targets.insert(variable.to_lowercase(), target);
|
||||
}
|
||||
if (trimmed.contains("Движения.") && trimmed.contains(".Записать"))
|
||||
|| (lowered.contains("movements.") && lowered.contains(".write"))
|
||||
{
|
||||
result.push(ParsedWrite {
|
||||
owner_procedure: owner.clone(),
|
||||
target: extract_register_target(trimmed),
|
||||
write_type: "REGISTER_WRITE".to_string(),
|
||||
source_range: SourceRange {
|
||||
line_start: line_no,
|
||||
line_end: line_no,
|
||||
column_start: 1,
|
||||
column_end: line.len() + 1,
|
||||
},
|
||||
});
|
||||
} else if let Some((target, write_type)) =
|
||||
extract_object_write_target(trimmed, &object_targets)
|
||||
{
|
||||
result.push(ParsedWrite {
|
||||
owner_procedure: owner.clone(),
|
||||
target,
|
||||
write_type,
|
||||
source_range: SourceRange {
|
||||
line_start: line_no,
|
||||
line_end: line_no,
|
||||
column_start: 1,
|
||||
column_end: line.len() + 1,
|
||||
},
|
||||
});
|
||||
} else if trimmed.contains(".Записать()") || trimmed.contains(".Write()") {
|
||||
result.push(ParsedWrite {
|
||||
owner_procedure: owner.clone(),
|
||||
target: "unknown".to_string(),
|
||||
write_type: "OBJECT_WRITE".to_string(),
|
||||
source_range: SourceRange {
|
||||
line_start: line_no,
|
||||
line_end: line_no,
|
||||
column_start: 1,
|
||||
column_end: line.len() + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn extract_register_target(line: &str) -> String {
|
||||
let lowered = line.to_lowercase();
|
||||
let after = if let Some(value) = line.split("Движения.").nth(1) {
|
||||
value
|
||||
} else if let Some(index) = lowered.find("movements.") {
|
||||
&line[index + "movements.".len()..]
|
||||
} else {
|
||||
return "unknown".to_string();
|
||||
};
|
||||
after.split('.').next().unwrap_or("unknown").to_string()
|
||||
}
|
||||
|
||||
fn extract_object_create_target(line: &str) -> Option<(String, String)> {
|
||||
let (variable, expression) = line.split_once('=')?;
|
||||
let variable = variable.trim();
|
||||
let expression = expression.trim();
|
||||
let parts: Vec<&str> = expression.split('.').collect();
|
||||
if parts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let prefix = if parts[0].eq_ignore_ascii_case("Справочники")
|
||||
|| parts[0].eq_ignore_ascii_case("Catalogs")
|
||||
{
|
||||
"Справочник"
|
||||
} else if parts[0].eq_ignore_ascii_case("Документы")
|
||||
|| parts[0].eq_ignore_ascii_case("Documents")
|
||||
{
|
||||
"Документ"
|
||||
} else if parts[0].eq_ignore_ascii_case("РегистрыСведений")
|
||||
|| parts[0].eq_ignore_ascii_case("InformationRegisters")
|
||||
{
|
||||
"РегистрСведений"
|
||||
} else if parts[0].eq_ignore_ascii_case("РегистрыНакопления")
|
||||
|| parts[0].eq_ignore_ascii_case("AccumulationRegisters")
|
||||
{
|
||||
"РегистрНакопления"
|
||||
} else if parts[0].eq_ignore_ascii_case("РегистрыБухгалтерии")
|
||||
|| parts[0].eq_ignore_ascii_case("AccountingRegisters")
|
||||
{
|
||||
"РегистрБухгалтерии"
|
||||
} else if parts[0].eq_ignore_ascii_case("РегистрыРасчета")
|
||||
|| parts[0].eq_ignore_ascii_case("CalculationRegisters")
|
||||
{
|
||||
"РегистрРасчета"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let method = parts[2].split('(').next()?.trim();
|
||||
let valid_method = method.eq_ignore_ascii_case("СоздатьЭлемент")
|
||||
|| method.eq_ignore_ascii_case("CreateItem")
|
||||
|| method.eq_ignore_ascii_case("СоздатьДокумент")
|
||||
|| method.eq_ignore_ascii_case("CreateDocument")
|
||||
|| method.eq_ignore_ascii_case("СоздатьНаборЗаписей")
|
||||
|| method.eq_ignore_ascii_case("CreateRecordSet");
|
||||
if !valid_method || variable.is_empty() || parts[1].trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
variable.to_string(),
|
||||
format!("{}.{}", prefix, parts[1].trim()),
|
||||
))
|
||||
}
|
||||
|
||||
fn extract_object_write_target(
|
||||
line: &str,
|
||||
object_targets: &HashMap<String, String>,
|
||||
) -> Option<(String, String)> {
|
||||
let (variable, method) = line.split_once('.')?;
|
||||
let method_name = method.split('(').next()?.trim();
|
||||
let is_write =
|
||||
method_name.eq_ignore_ascii_case("Записать") || method_name.eq_ignore_ascii_case("Write");
|
||||
if !is_write {
|
||||
return None;
|
||||
}
|
||||
let target = object_targets.get(&variable.trim().to_lowercase())?.clone();
|
||||
let write_type = if target.to_lowercase().starts_with("регистр") {
|
||||
"REGISTER_WRITE"
|
||||
} else {
|
||||
"OBJECT_WRITE"
|
||||
};
|
||||
Some((target, write_type.to_string()))
|
||||
}
|
||||
|
||||
fn strip_inline_comment(line: &str) -> &str {
|
||||
line.split("//").next().unwrap_or(line)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "query-parser"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
@@ -0,0 +1,143 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ParsedQueryText {
|
||||
pub tables: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn parse_query_text(query_text: &str) -> ParsedQueryText {
|
||||
ParsedQueryText {
|
||||
tables: extract_tables(query_text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_tables(query_text: &str) -> Vec<String> {
|
||||
let mut tables = Vec::new();
|
||||
let lines: Vec<String> = query_text.lines().map(clean_query_line).collect();
|
||||
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
if let Some(table) = table_after_from(line) {
|
||||
push_unique(&mut tables, table);
|
||||
} else if is_standalone_from(line) {
|
||||
if let Some(next_line) = lines.get(index + 1) {
|
||||
if let Some(table) = first_table_token(next_line) {
|
||||
push_unique(&mut tables, table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(table) = table_after_join(line) {
|
||||
push_unique(&mut tables, table);
|
||||
}
|
||||
}
|
||||
|
||||
tables
|
||||
}
|
||||
|
||||
fn clean_query_line(line: &str) -> String {
|
||||
let mut value = line
|
||||
.trim()
|
||||
.trim_end_matches(';')
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim();
|
||||
if let Some(stripped) = value.strip_prefix('|') {
|
||||
value = stripped.trim();
|
||||
}
|
||||
value.trim_matches('"').trim().to_string()
|
||||
}
|
||||
|
||||
fn is_standalone_from(line: &str) -> bool {
|
||||
line.eq_ignore_ascii_case("ИЗ") || line.eq_ignore_ascii_case("FROM")
|
||||
}
|
||||
|
||||
fn table_after_from(line: &str) -> Option<String> {
|
||||
let trimmed = line.trim();
|
||||
let upper = trimmed.to_uppercase();
|
||||
let rest = if upper.starts_with("ИЗ ") {
|
||||
&trimmed["ИЗ".len()..]
|
||||
} else if upper.starts_with("FROM ") {
|
||||
&trimmed["FROM".len()..]
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
first_table_token(rest)
|
||||
}
|
||||
|
||||
fn table_after_join(line: &str) -> Option<String> {
|
||||
let normalized = line.replace('\t', " ");
|
||||
let parts: Vec<&str> = normalized.split_whitespace().collect();
|
||||
let index = parts.iter().position(|part| {
|
||||
part.eq_ignore_ascii_case("JOIN") || part.eq_ignore_ascii_case("СОЕДИНЕНИЕ")
|
||||
})?;
|
||||
let table = parts.get(index + 1)?.trim_matches(',');
|
||||
if table.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(table.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn first_table_token(line: &str) -> Option<String> {
|
||||
let table = line.trim().split_whitespace().next()?.trim_matches(',');
|
||||
if table.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(table.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique(values: &mut Vec<String>, value: String) {
|
||||
if !values.iter().any(|existing| existing == &value) {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{extract_tables, parse_query_text};
|
||||
|
||||
#[test]
|
||||
fn extracts_table_after_standalone_from() {
|
||||
let tables = extract_tables(
|
||||
r#"
|
||||
ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(tables, vec!["РегистрНакопления.ОстаткиТоваров"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_inline_from_and_join_tables() {
|
||||
let query = parse_query_text(
|
||||
r#"
|
||||
SELECT
|
||||
Orders.Ref,
|
||||
Customers.Description
|
||||
FROM Document.CustomerOrder AS Orders
|
||||
LEFT JOIN Catalog.Customers AS Customers
|
||||
ON Orders.Customer = Customers.Ref
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
query.tables,
|
||||
vec!["Document.CustomerOrder", "Catalog.Customers"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleans_pipe_prefixed_1c_query_lines() {
|
||||
let tables = extract_tables(
|
||||
r#"
|
||||
|ВЫБРАТЬ
|
||||
|Контрагенты.Ссылка
|
||||
|ИЗ Справочник.Контрагенты КАК Контрагенты
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(tables, vec!["Справочник.Контрагенты"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "semantic-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
@@ -0,0 +1,181 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SemanticNode {
|
||||
pub lineage_id: String,
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub qualified_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SemanticEdge {
|
||||
pub edge_id: String,
|
||||
pub kind: String,
|
||||
pub source_lineage: String,
|
||||
pub target_lineage: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct SemanticGraph {
|
||||
nodes: BTreeMap<String, SemanticNode>,
|
||||
outgoing: BTreeMap<String, Vec<SemanticEdge>>,
|
||||
incoming: BTreeMap<String, Vec<SemanticEdge>>,
|
||||
}
|
||||
|
||||
impl SemanticGraph {
|
||||
pub fn new(nodes: Vec<SemanticNode>, edges: Vec<SemanticEdge>) -> Self {
|
||||
let nodes_by_lineage = nodes
|
||||
.into_iter()
|
||||
.map(|node| (node.lineage_id.clone(), node))
|
||||
.collect();
|
||||
let mut graph = Self {
|
||||
nodes: nodes_by_lineage,
|
||||
outgoing: BTreeMap::new(),
|
||||
incoming: BTreeMap::new(),
|
||||
};
|
||||
for edge in edges {
|
||||
graph.add_edge(edge);
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
pub fn add_edge(&mut self, edge: SemanticEdge) {
|
||||
self.outgoing
|
||||
.entry(edge.source_lineage.clone())
|
||||
.or_default()
|
||||
.push(edge.clone());
|
||||
self.incoming
|
||||
.entry(edge.target_lineage.clone())
|
||||
.or_default()
|
||||
.push(edge);
|
||||
}
|
||||
|
||||
pub fn find_callers(&self, routine_name: &str) -> Vec<&SemanticNode> {
|
||||
let target_lineages = self.routine_lineages(routine_name);
|
||||
let caller_lineages = target_lineages
|
||||
.iter()
|
||||
.flat_map(|lineage| self.incoming.get(lineage).into_iter().flatten())
|
||||
.filter(|edge| edge.kind == "CALLS")
|
||||
.map(|edge| edge.source_lineage.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
caller_lineages
|
||||
.iter()
|
||||
.filter_map(|lineage| self.nodes.get(*lineage))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_callees(&self, routine_name: &str) -> Vec<&SemanticNode> {
|
||||
self.routine_lineages(routine_name)
|
||||
.iter()
|
||||
.flat_map(|lineage| self.outgoing.get(lineage).into_iter().flatten())
|
||||
.filter(|edge| edge.kind == "CALLS")
|
||||
.filter_map(|edge| self.nodes.get(&edge.target_lineage))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_writes(&self, routine_name: &str) -> Vec<&SemanticNode> {
|
||||
self.targets_by_edge_kind(routine_name, "WRITES")
|
||||
}
|
||||
|
||||
pub fn find_reads(&self, routine_name: &str) -> Vec<&SemanticNode> {
|
||||
let query_lineages = self
|
||||
.routine_lineages(routine_name)
|
||||
.iter()
|
||||
.flat_map(|lineage| self.outgoing.get(lineage).into_iter().flatten())
|
||||
.filter(|edge| edge.kind == "OWNS_QUERY")
|
||||
.map(|edge| edge.target_lineage.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
query_lineages
|
||||
.iter()
|
||||
.flat_map(|lineage| self.outgoing.get(*lineage).into_iter().flatten())
|
||||
.filter(|edge| edge.kind == "READS_TABLE")
|
||||
.filter_map(|edge| self.nodes.get(&edge.target_lineage))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn targets_by_edge_kind(&self, routine_name: &str, kind: &str) -> Vec<&SemanticNode> {
|
||||
self.routine_lineages(routine_name)
|
||||
.iter()
|
||||
.flat_map(|lineage| self.outgoing.get(lineage).into_iter().flatten())
|
||||
.filter(|edge| edge.kind == kind)
|
||||
.filter_map(|edge| self.nodes.get(&edge.target_lineage))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn routine_lineages(&self, routine_name: &str) -> BTreeSet<String> {
|
||||
let wanted = routine_name.to_lowercase();
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|node| {
|
||||
matches!(node.kind.as_str(), "PROCEDURE" | "FUNCTION")
|
||||
&& node.name.to_lowercase() == wanted
|
||||
})
|
||||
.map(|node| node.lineage_id.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{SemanticEdge, SemanticGraph, SemanticNode};
|
||||
|
||||
#[test]
|
||||
fn resolves_callers_callees_reads_and_writes() {
|
||||
let graph = SemanticGraph::new(
|
||||
vec![
|
||||
node("routine.post", "PROCEDURE", "Проведение"),
|
||||
node("routine.check", "FUNCTION", "ПроверитьОстатки"),
|
||||
node("query.check.1", "QUERY", "ПроверитьОстатки.query1"),
|
||||
node("table.stock", "REGISTER", "ОстаткиТоваров"),
|
||||
],
|
||||
vec![
|
||||
edge("e1", "CALLS", "routine.post", "routine.check"),
|
||||
edge("e2", "WRITES", "routine.post", "table.stock"),
|
||||
edge("e3", "OWNS_QUERY", "routine.check", "query.check.1"),
|
||||
edge("e4", "READS_TABLE", "query.check.1", "table.stock"),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
names(graph.find_callees("Проведение")),
|
||||
vec!["ПроверитьОстатки"]
|
||||
);
|
||||
assert_eq!(
|
||||
names(graph.find_callers("ПроверитьОстатки")),
|
||||
vec!["Проведение"]
|
||||
);
|
||||
assert_eq!(
|
||||
names(graph.find_writes("Проведение")),
|
||||
vec!["ОстаткиТоваров"]
|
||||
);
|
||||
assert_eq!(
|
||||
names(graph.find_reads("ПроверитьОстатки")),
|
||||
vec!["ОстаткиТоваров"]
|
||||
);
|
||||
}
|
||||
|
||||
fn node(lineage_id: &str, kind: &str, name: &str) -> SemanticNode {
|
||||
SemanticNode {
|
||||
lineage_id: lineage_id.to_string(),
|
||||
kind: kind.to_string(),
|
||||
name: name.to_string(),
|
||||
qualified_name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn edge(edge_id: &str, kind: &str, source: &str, target: &str) -> SemanticEdge {
|
||||
SemanticEdge {
|
||||
edge_id: edge_id.to_string(),
|
||||
kind: kind.to_string(),
|
||||
source_lineage: source.to_string(),
|
||||
target_lineage: target.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn names(nodes: Vec<&SemanticNode>) -> Vec<String> {
|
||||
nodes.into_iter().map(|node| node.name.clone()).collect()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user