2025-11-04 16:33:04 +09:00
//! 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 ( ) ,
2025-11-06 15:41:52 +09:00
strict : crate ::config ::env ::env_bool ( " NYASH_USING_STRICT " ) ,
2025-11-04 16:33:04 +09:00
verbose : crate ::config ::env ::cli_verbose ( )
2025-11-06 15:41:52 +09:00
| | crate ::config ::env ::env_bool ( " NYASH_RESOLVE_TRACE " ) ,
2025-11-04 16:33:04 +09:00
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 {
2025-11-06 15:41:52 +09:00
crate ::runner ::modes ::common_util ::resolve ::path_util ::is_using_target_path_unquoted ( & target_unquoted )
2025-11-04 16:33:04 +09:00
} ;
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
}
}