Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+73
View File
@@ -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)
}
+24
View File
@@ -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")
}
+290
View File
@@ -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, "ОстаткиТоваров");
}
}
+33
View File
@@ -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!();
}
+59
View File
@@ -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>,
}
+85
View File
@@ -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()
}
+121
View File
@@ -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)
}
+159
View File
@@ -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)
}