223 lines
7.8 KiB
Rust
223 lines
7.8 KiB
Rust
|
|
//! Using Resolution Box - 綺麗綺麗なusing文解決専門家!📦
|
|||
|
|
//!
|
|||
|
|
//! 巨大な `collect_using_and_strip` 関数を箱に分解して、
|
|||
|
|
//! 責務を明確にしてテストしやすくするにゃ!
|
|||
|
|
|
|||
|
|
use crate::runner::NyashRunner;
|
|||
|
|
use std::collections::HashMap;
|
|||
|
|
use std::path::{Path, PathBuf};
|
|||
|
|
|
|||
|
|
/// 📦 UsingResolutionBox - using文解決の専門家!
|
|||
|
|
///
|
|||
|
|
/// using文の解析、パス解決、重複チェックを一手に引き受ける箱にゃ!
|
|||
|
|
pub struct UsingResolutionBox<'a> {
|
|||
|
|
runner: &'a NyashRunner,
|
|||
|
|
config: UsingConfig,
|
|||
|
|
ctx_dir: Option<PathBuf>,
|
|||
|
|
filename_canon: Option<PathBuf>,
|
|||
|
|
inside_pkg: bool,
|
|||
|
|
seen_paths: HashMap<String, (String, usize)>, // canon_path -> (alias/label, first_line)
|
|||
|
|
seen_aliases: HashMap<String, (String, usize)>, // alias -> (canon_path, first_line)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 🎯 UsingTarget - 解析済みusing文の構造体にゃ!
|
|||
|
|
#[derive(Debug, Clone)]
|
|||
|
|
pub struct UsingTarget {
|
|||
|
|
pub original: String,
|
|||
|
|
pub target: String,
|
|||
|
|
pub target_unquoted: String,
|
|||
|
|
pub alias: Option<String>,
|
|||
|
|
pub line_no: usize,
|
|||
|
|
pub is_path: bool,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// ⚙️ UsingConfig - using解決の設定!
|
|||
|
|
#[derive(Debug)]
|
|||
|
|
pub struct UsingConfig {
|
|||
|
|
pub prod: bool,
|
|||
|
|
pub strict: bool,
|
|||
|
|
pub verbose: bool,
|
|||
|
|
pub allow_file_using: bool,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl<'a> UsingResolutionBox<'a> {
|
|||
|
|
/// 🌟 新しいUsingResolutionBoxを作るにゃ!
|
|||
|
|
pub fn new(runner: &'a NyashRunner, filename: &str) -> Result<Self, String> {
|
|||
|
|
let using_ctx = runner.init_using_context();
|
|||
|
|
let config = UsingConfig {
|
|||
|
|
prod: crate::config::env::using_is_prod(),
|
|||
|
|
strict: std::env::var("NYASH_USING_STRICT").ok().as_deref() == Some("1"),
|
|||
|
|
verbose: crate::config::env::cli_verbose()
|
|||
|
|
|| std::env::var("NYASH_RESOLVE_TRACE").ok().as_deref() == Some("1"),
|
|||
|
|
allow_file_using: crate::config::env::allow_using_file(),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let ctx_dir = Path::new(filename).parent().map(|p| p.to_path_buf());
|
|||
|
|
|
|||
|
|
// ファイルがパッケージ内にあるかチェック
|
|||
|
|
let filename_canon = std::fs::canonicalize(filename).ok();
|
|||
|
|
let mut inside_pkg = false;
|
|||
|
|
if let Some(ref fc) = filename_canon {
|
|||
|
|
for (_name, pkg) in &using_ctx.packages {
|
|||
|
|
let base = Path::new(&pkg.path);
|
|||
|
|
if let Ok(root) = std::fs::canonicalize(base) {
|
|||
|
|
if fc.starts_with(&root) {
|
|||
|
|
inside_pkg = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Ok(Self {
|
|||
|
|
runner,
|
|||
|
|
config,
|
|||
|
|
ctx_dir,
|
|||
|
|
filename_canon,
|
|||
|
|
inside_pkg,
|
|||
|
|
seen_paths: HashMap::new(),
|
|||
|
|
seen_aliases: HashMap::new(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 🔍 using文を解析するにゃ!
|
|||
|
|
pub fn parse_using_line(&self, line: &str, line_no: usize) -> Option<UsingTarget> {
|
|||
|
|
let t = line.trim_start();
|
|||
|
|
if !t.starts_with("using ") {
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
crate::cli_v!("[using] stripped line: {}", line);
|
|||
|
|
|
|||
|
|
let rest0 = t.strip_prefix("using ").unwrap().trim();
|
|||
|
|
let rest0 = rest0.split('#').next().unwrap_or(rest0).trim();
|
|||
|
|
let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
|
|||
|
|
|
|||
|
|
let (target, alias) = if let Some(pos) = rest0.find(" as ") {
|
|||
|
|
(
|
|||
|
|
rest0[..pos].trim().to_string(),
|
|||
|
|
Some(rest0[pos + 4..].trim().to_string()),
|
|||
|
|
)
|
|||
|
|
} else {
|
|||
|
|
(rest0.to_string(), None)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let target_unquoted = target.trim_matches('"').to_string();
|
|||
|
|
let using_ctx = self.runner.init_using_context();
|
|||
|
|
|
|||
|
|
// 既知のエイリアスかモジュールかチェック
|
|||
|
|
let is_known_alias_or_module = using_ctx.aliases.contains_key(&target_unquoted)
|
|||
|
|
|| using_ctx.pending_modules.iter().any(|(k, _)| k == &target_unquoted)
|
|||
|
|
|| using_ctx.packages.contains_key(&target_unquoted);
|
|||
|
|
|
|||
|
|
let is_path = if is_known_alias_or_module {
|
|||
|
|
false
|
|||
|
|
} else {
|
|||
|
|
target.starts_with("./")
|
|||
|
|
|| target.starts_with('/')
|
|||
|
|
|| target.ends_with(".nyash")
|
|||
|
|
|| target.ends_with(".hako")
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Some(UsingTarget {
|
|||
|
|
original: line.to_string(),
|
|||
|
|
target,
|
|||
|
|
target_unquoted,
|
|||
|
|
alias,
|
|||
|
|
line_no,
|
|||
|
|
is_path,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 🚀 パスを解決するにゃ!
|
|||
|
|
pub fn resolve_path(&self, target: &UsingTarget) -> Result<String, String> {
|
|||
|
|
if !target.is_path {
|
|||
|
|
return Err("Not a file path".to_string());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ファイルusingチェック
|
|||
|
|
if (self.config.prod || !self.config.allow_file_using) && !self.inside_pkg {
|
|||
|
|
return Err(format!(
|
|||
|
|
"{}:{}: using: file paths are disallowed in this profile. Add it to nyash.toml [using]/[modules] and reference by name: {}\n suggestions: using \"alias.name\" as Name | dev/test: set NYASH_PREINCLUDE=1 to expand includes ahead of VM\n docs: see docs/reference/using.md",
|
|||
|
|
"filename", // TODO: 実際のファイル名を渡す
|
|||
|
|
target.line_no,
|
|||
|
|
target.target
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let path = target.target.trim_matches('"').to_string();
|
|||
|
|
let mut p = PathBuf::from(&path);
|
|||
|
|
|
|||
|
|
// 相対パス解決
|
|||
|
|
if p.is_relative() {
|
|||
|
|
if let Some(dir) = &self.ctx_dir {
|
|||
|
|
let cand = dir.join(&p);
|
|||
|
|
if cand.exists() {
|
|||
|
|
p = cand;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NYASH_ROOTも試す
|
|||
|
|
if p.is_relative() {
|
|||
|
|
if let Ok(root) = std::env::var("NYASH_ROOT") {
|
|||
|
|
let cand = Path::new(&root).join(&p);
|
|||
|
|
if cand.exists() {
|
|||
|
|
p = cand;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
p.to_str()
|
|||
|
|
.ok_or_else(|| "Invalid path".to_string())
|
|||
|
|
.map(|s| s.to_string())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 🛡️ 重複チェックするにゃ!
|
|||
|
|
pub fn check_duplicates(&mut self, target: &UsingTarget, resolved_path: &str) -> Result<(), String> {
|
|||
|
|
let canon_path = std::fs::canonicalize(resolved_path)
|
|||
|
|
.unwrap_or_else(|_| PathBuf::from(resolved_path));
|
|||
|
|
let canon_str = canon_path.to_string_lossy();
|
|||
|
|
|
|||
|
|
// パスの重複チェック
|
|||
|
|
if let Some((prev_alias, prev_line)) = self.seen_paths.get(&canon_str.to_string()) {
|
|||
|
|
return Err(format!(
|
|||
|
|
"{}:{}: using: duplicate target (first imported at {}:{})",
|
|||
|
|
"filename", // TODO: 実際のファイル名を渡す
|
|||
|
|
target.line_no,
|
|||
|
|
prev_alias,
|
|||
|
|
prev_line
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// エイリアスの重複チェック
|
|||
|
|
if let Some(ref alias_name) = target.alias {
|
|||
|
|
if let Some((prev_path, prev_line)) = self.seen_aliases.get(alias_name) {
|
|||
|
|
return Err(format!(
|
|||
|
|
"{}:{}: using: duplicate alias '{}' (first used for {} at {})",
|
|||
|
|
"filename", // TODO: 実際のファイル名を渡す
|
|||
|
|
target.line_no,
|
|||
|
|
alias_name,
|
|||
|
|
prev_path,
|
|||
|
|
prev_line
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 記録
|
|||
|
|
let alias_label = target.alias.as_ref().unwrap_or(&target.target).clone();
|
|||
|
|
self.seen_paths.insert(canon_str.to_string(), (alias_label.clone(), target.line_no));
|
|||
|
|
|
|||
|
|
if let Some(ref alias_name) = target.alias {
|
|||
|
|
self.seen_aliases.insert(alias_name.clone(), (resolved_path.to_string(), target.line_no));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Ok(())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 📊 設定を取得するにゃ!
|
|||
|
|
pub fn config(&self) -> &UsingConfig {
|
|||
|
|
&self.config
|
|||
|
|
}
|
|||
|
|
}
|