/*! * Runner pipeline helpers — using/modules/env pre-processing * * Extracts the early-phase setup from runner/mod.rs: * - load nyash.toml [modules] and [using.paths] * - merge with defaults and env overrides * - expose context (using_paths, pending_modules) for downstream resolution */ use super::*; use crate::using::spec::{PackageKind, UsingPackage}; use crate::using::ssot_bridge::{call_using_resolve_ssot, SsotCtx}; use std::collections::HashMap; /// Using/module resolution context accumulated from config/env/nyash.toml pub(super) struct UsingContext { pub using_paths: Vec, pub pending_modules: Vec<(String, String)>, pub aliases: std::collections::HashMap, pub packages: std::collections::HashMap, } impl NyashRunner { /// Initialize using/module context from defaults, nyash.toml and env pub(super) fn init_using_context(&self) -> UsingContext { let mut using_paths: Vec = Vec::new(); let mut pending_modules: Vec<(String, String)> = Vec::new(); let mut aliases: std::collections::HashMap = std::collections::HashMap::new(); let mut packages: std::collections::HashMap = std::collections::HashMap::new(); // Defaults using_paths.extend(["apps", "lib", "."].into_iter().map(|s| s.to_string())); // nyash.toml: delegate to using resolver (keeps existing behavior) let toml_result = crate::using::resolver::populate_from_toml( &mut using_paths, &mut pending_modules, &mut aliases, &mut packages, ); // 🔍 Debug: Check if aliases are loaded if std::env::var("NYASH_DEBUG_USING").ok().as_deref() == Some("1") { eprintln!("[DEBUG/using] populate_from_toml result: {:?}", toml_result); eprintln!("[DEBUG/using] Loaded {} aliases", aliases.len()); for (k, v) in aliases.iter() { eprintln!("[DEBUG/using] alias: '{}' => '{}'", k, v); } eprintln!("[DEBUG/using] Loaded {} packages", packages.len()); for (k, v) in packages.iter() { eprintln!("[DEBUG/using] package: '{}' => path='{}'", k, v.path); } } // ⚠️ Phase 0.1: Immediate error display for TOML parse failures if let Err(e) = &toml_result { eprintln!("⚠️ [using/workspace] Failed to load TOML modules:"); eprintln!(" Error: {}", e); eprintln!(" → All 'using' aliases will be unavailable"); eprintln!(" → Fix TOML syntax errors in workspace modules"); eprintln!(); eprintln!(" 💡 Debug: NYASH_DEBUG_USING=1 for detailed logs"); } // Env overrides: modules and using paths if let Ok(ms) = std::env::var("NYASH_MODULES") { for ent in ms.split(',') { if let Some((k, v)) = ent.split_once('=') { let k = k.trim(); let v = v.trim(); if !k.is_empty() && !v.is_empty() { pending_modules.push((k.to_string(), v.to_string())); } } } } if let Ok(p) = std::env::var("NYASH_USING_PATH") { for s in p.split(':') { let s = s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); } } } // Env aliases: comma-separated k=v pairs if let Ok(raw) = std::env::var("NYASH_ALIASES") { for ent in raw.split(',') { if let Some((k, v)) = ent.split_once('=') { let k = k.trim(); let v = v.trim(); if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); } } } } UsingContext { using_paths, pending_modules, aliases, packages, } } } /// Suggest candidate files by leaf name within limited bases (apps/lib/.) #[allow(dead_code)] pub(super) fn suggest_in_base(base: &str, leaf: &str, out: &mut Vec) { use std::fs; fn walk(dir: &std::path::Path, leaf: &str, out: &mut Vec, depth: usize) { if depth == 0 || out.len() >= 5 { return; } if let Ok(entries) = fs::read_dir(dir) { for e in entries.flatten() { let path = e.path(); if path.is_dir() { walk(&path, leaf, out, depth - 1); if out.len() >= 5 { return; } } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) { if ext == "nyash" || ext == "hako" { if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { if stem == leaf { out.push(path.to_string_lossy().to_string()); if out.len() >= 5 { return; } } } } } } } } let p = std::path::Path::new(base); walk(p, leaf, out, 4); } /// Resolve a using target according to priority: modules > relative > using-paths /// Returns Ok(resolved_path_or_token). On strict mode, ambiguous matches cause error. pub(super) fn resolve_using_target( tgt: &str, is_path: bool, modules: &[(String, String)], using_paths: &[String], aliases: &HashMap, packages: &HashMap, context_dir: Option<&std::path::Path>, strict: bool, verbose: bool, ) -> Result { // Dev toggle: try SSOT common resolver first, then fall back to legacy path. // This helps migrate behavior gradually without changing defaults. if std::env::var("HAKO_USING_RESOLVER_FIRST").ok().as_deref() == Some("1") { match crate::using::resolver::resolve_using_target_common( tgt, modules, using_paths, packages, context_dir, strict, verbose, ) { Ok(val) => return Ok(val), Err(_) => { /* fall through to legacy path */ } } } // Phase 22.1: Thin SSOT hook (future wiring). No behavior change for now. if std::env::var("HAKO_USING_SSOT").ok().as_deref() == Some("1") && std::env::var("HAKO_USING_SSOT_INVOKING").ok().as_deref() != Some("1") { if let Some(ssot_res) = try_resolve_using_target_ssot( tgt, is_path, modules, using_paths, aliases, packages, context_dir, strict, verbose, ) { return Ok(ssot_res); } } // Invalidate and rebuild index/cache if env or nyash.toml changed super::box_index::rebuild_if_env_changed(); if is_path { return Ok(tgt.to_string()); } let trace = verbose || crate::config::env::env_bool("NYASH_RESOLVE_TRACE"); let idx = super::box_index::get_box_index(); let mut strict_effective = strict || idx.plugins_require_prefix_global; if crate::config::env::env_bool("NYASH_PLUGIN_REQUIRE_PREFIX") { strict_effective = true; } let meta_for_target = idx.plugin_meta_by_box.get(tgt).cloned(); let mut require_prefix_target = meta_for_target .as_ref() .map(|m| m.require_prefix) .unwrap_or(false); if let Some(m) = &meta_for_target { if !m.expose_short_names { require_prefix_target = true; } } let mut is_plugin_short = meta_for_target.is_some(); if !is_plugin_short { is_plugin_short = idx.plugin_boxes.contains(tgt) || super::box_index::BoxIndex::is_known_plugin_short(tgt); } if (strict_effective || require_prefix_target) && is_plugin_short && !tgt.contains('.') { let mut msg = format!("plugin short name '{}' requires prefix", tgt); if let Some(meta) = &meta_for_target { if let Some(pref) = &meta.prefix { msg.push_str(&format!(" (use '{}.{}')", pref, tgt)); } } return Err(msg); } let key = { let base = context_dir.and_then(|p| p.to_str()).unwrap_or(""); format!( "{}|{}|{}|{}", tgt, base, strict as i32, using_paths.join(":") ) }; if let Some(hit) = crate::runner::box_index::cache_get(&key) { if trace { crate::runner::trace::log(format!("[using/cache] '{}' -> '{}'", tgt, hit)); } return Ok(hit); } // Resolve aliases early(推移的に) // - ループ/循環を検出して早期エラー // - 10段まで(防衛的) if let Some(_) = aliases.get(tgt) { use std::collections::HashSet; let mut seen: HashSet = HashSet::new(); let mut cur = tgt.to_string(); let mut depth = 0usize; while let Some(next) = aliases.get(&cur).cloned() { if trace { crate::runner::trace::log(format!("[using/resolve] alias '{}' -> '{}'", cur, next)); } if !seen.insert(cur.clone()) { return Err(format!("alias cycle detected at '{}'", cur)); } cur = next; depth += 1; if depth > 10 { return Err(format!("alias resolution too deep starting at '{}'", tgt)); } // Continue while next is also an alias; break when concrete if !aliases.contains_key(&cur) { break; } } // Recurse once into final target to materialize path/token let rec = resolve_using_target( &cur, false, modules, using_paths, aliases, packages, context_dir, strict, verbose, )?; crate::runner::box_index::cache_put(&key, rec.clone()); return Ok(rec); } // Named packages (nyash.toml [using.]) if let Some(pkg) = packages.get(tgt) { match pkg.kind { PackageKind::Dylib => { // Return a marker token to avoid inlining attempts; loader will consume later stages let out = format!("dylib:{}", pkg.path); if trace { crate::runner::trace::log(format!( "[using/resolve] dylib '{}' -> '{}'", tgt, out )); } crate::runner::box_index::cache_put(&key, out.clone()); return Ok(out); } PackageKind::Package => { // Compute entry: main or .hako let base = std::path::Path::new(&pkg.path); let out = if let Some(m) = &pkg.main { if matches!( base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako") ) { // path is a file; ignore main and use as-is pkg.path.clone() } else { base.join(m).to_string_lossy().to_string() } } else { if matches!( base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako") ) { pkg.path.clone() } else { let leaf = base.file_name().and_then(|s| s.to_str()).unwrap_or(tgt); // prefer .hako when package path points to a directory without explicit main let hako = base.join(format!("{}.hako", leaf)); if hako.exists() { hako.to_string_lossy().to_string() } else { base.join(format!("{}.hako", leaf)) .to_string_lossy() .to_string() } } }; if trace { crate::runner::trace::log(format!( "[using/resolve] package '{}' -> '{}'", tgt, out )); } crate::runner::box_index::cache_put(&key, out.clone()); return Ok(out); } } } // Also consult env aliases if let Ok(raw) = std::env::var("NYASH_ALIASES") { for ent in raw.split(',') { if let Some((k, v)) = ent.split_once('=') { if k.trim() == tgt { let out = v.trim().to_string(); if trace { crate::runner::trace::log(format!( "[using/resolve] env-alias '{}' -> '{}'", tgt, out )); } crate::runner::box_index::cache_put(&key, out.clone()); return Ok(out); } } } } // 2) Special handling for built-in namespaces if tgt == "nyashstd" { let out = "builtin:nyashstd".to_string(); if trace { crate::runner::trace::log(format!("[using/resolve] builtin '{}' -> '{}'", tgt, out)); } crate::runner::box_index::cache_put(&key, out.clone()); return Ok(out); } // 3) delegate resolution to using::resolver (SSOT) match crate::using::resolver::resolve_using_target_common( tgt, modules, using_paths, packages, context_dir, strict, verbose, ) { Ok(val) => { crate::runner::box_index::cache_put(&key, val.clone()); Ok(val) } Err(e) => { // Maintain previous behavior: return original name and log when unresolved if trace { crate::runner::trace::log(format!("[using] unresolved '{}' ({})", tgt, e)); } else { eprintln!("[using] not found: '{}'", tgt); } Ok(tgt.to_string()) } } } /// Thin SSOT wrapper — returns Some(resolved) when an alternative SSOT path is available. /// MVP: return None to keep current behavior. Future: call into Hako `UsingResolveSSOTBox`. #[allow(clippy::too_many_arguments)] fn try_resolve_using_target_ssot( tgt: &str, is_path: bool, modules: &[(String, String)], using_paths: &[String], aliases: &HashMap, packages: &HashMap, context_dir: Option<&std::path::Path>, strict: bool, verbose: bool, ) -> Option { // Phase 22.1 MVP: Build context and consult SSOT bridge (modules-only). let trace = verbose || crate::config::env::env_bool("NYASH_RESOLVE_TRACE"); let mut map: HashMap = HashMap::new(); for (k, v) in modules { map.insert(k.clone(), v.clone()); } let cwd_str = context_dir.and_then(|p| p.to_str()).map(|s| s.to_string()); let ctx = SsotCtx { modules: map, using_paths: using_paths.to_vec(), cwd: cwd_str, }; if let Some(hit) = call_using_resolve_ssot(tgt, &ctx) { if trace { crate::runner::trace::log(format!("[using/ssot] '{}' -> '{}'", tgt, hit)); } return Some(hit); } // Optional relative inference (Runner-side, guarded): prefer cwd > using_paths if std::env::var("HAKO_USING_SSOT_RELATIVE").ok().as_deref() == Some("1") { let rel_hako = tgt.replace('.', "/") + ".hako"; let rel_ny = tgt.replace('.', "/") + ".nyash"; let mut try_paths: Vec = Vec::new(); if let Some(dir) = context_dir { try_paths.push(dir.join(&rel_hako)); try_paths.push(dir.join(&rel_ny)); } for base in using_paths { let p = std::path::Path::new(base); try_paths.push(p.join(&rel_hako)); try_paths.push(p.join(&rel_ny)); } let mut found: Vec = Vec::new(); for p in try_paths { if p.exists() { found.push(p.to_string_lossy().to_string()); } } if !found.is_empty() { if found.len() > 1 && strict { if trace { let total = found.len(); // Allow customizing the number of shown candidates via env (bounded 1..=10) let n_show: usize = std::env::var("HAKO_USING_SSOT_RELATIVE_AMBIG_FIRST_N") .ok() .and_then(|s| s.parse::().ok()) .map(|n| n.clamp(1, 10)) .unwrap_or(3); let shown: Vec = found.iter().take(n_show).cloned().collect(); // Standardized message: count + first N + explicit delegation policy crate::runner::trace::log(format!( "[using/ssot:relative ambiguous] name='{}' count={} first=[{}] -> delegate=legacy(strict)", tgt, total, shown.join(", ") )); } // Strict ambiguity: delegate to legacy resolver (behavior unchanged) } else { let out = found.remove(0); if trace { crate::runner::trace::log(format!( "[using/ssot:relative] '{}' -> '{}' (priority=cwd>using_paths)", tgt, out )); } return Some(out); } } } // Fallback: keep parity by delegating to existing resolver within the same gate let prev = std::env::var("HAKO_USING_SSOT_INVOKING").ok(); std::env::set_var("HAKO_USING_SSOT_INVOKING", "1"); let res = resolve_using_target( tgt, is_path, modules, using_paths, aliases, packages, context_dir, strict, verbose, ); if let Some(val) = prev { std::env::set_var("HAKO_USING_SSOT_INVOKING", val); } else { let _ = std::env::remove_var("HAKO_USING_SSOT_INVOKING"); } res.ok() } /// Lint: enforce "fields must be at the top of box" rule. /// - Warns by default (when verbose); when `strict` is true, returns Err on any violation. pub(super) fn lint_fields_top(code: &str, strict: bool, verbose: bool) -> Result<(), String> { let mut brace: i32 = 0; let mut in_box = false; let mut box_depth: i32 = 0; let mut seen_method = false; let mut cur_box: String = String::new(); let mut violations: Vec<(usize, String, String)> = Vec::new(); // (line, field, box) for (idx, line) in code.lines().enumerate() { let lno = idx + 1; let pre_brace = brace; let trimmed = line.trim(); // Count braces for this line let opens = line.matches('{').count() as i32; let closes = line.matches('}').count() as i32; // Enter box on same-line K&R style: `box Name {` or `static box Name {` if !in_box && trimmed.starts_with("box ") || trimmed.starts_with("static box ") { // capture name let mut name = String::new(); let after = if let Some(rest) = trimmed.strip_prefix("static box ") { rest } else { trimmed.strip_prefix("box ").unwrap_or("") }; for ch in after.chars() { if ch.is_alphanumeric() || ch == '_' { name.push(ch); } else { break; } } // require K&R brace on same line to start tracking if opens > 0 { in_box = true; cur_box = name; box_depth = pre_brace + 1; // assume one level for box body seen_method = false; } } if in_box { // Top-level inside box only if pre_brace == box_depth { // Skip empty/comment lines if !trimmed.is_empty() && !trimmed.starts_with("//") { // Detect method: name(args) { let is_method = { // starts with identifier then '(' and later '{' let mut it = trimmed.chars(); let mut ident = String::new(); while let Some(c) = it.next() { if c.is_whitespace() { continue; } if c.is_alphabetic() || c == '_' { ident.push(c); break; } else { break; } } while let Some(c) = it.next() { if c.is_alphanumeric() || c == '_' { ident.push(c); } else { break; } } trimmed.contains('(') && trimmed.ends_with('{') && !ident.is_empty() }; if is_method { seen_method = true; } // Detect field: ident ':' Type (rough heuristic) let is_field = { let parts: Vec<&str> = trimmed.split(':').collect(); if parts.len() == 2 { let lhs = parts[0].trim(); let rhs = parts[1].trim(); let lhs_ok = !lhs.is_empty() && lhs .chars() .next() .map(|c| c.is_alphabetic() || c == '_') .unwrap_or(false); let rhs_ok = !rhs.is_empty() && rhs .chars() .next() .map(|c| c.is_alphabetic() || c == '_') .unwrap_or(false); lhs_ok && rhs_ok && !trimmed.contains('(') && !trimmed.contains(')') } else { false } }; if is_field && seen_method { violations.push((lno, trimmed.to_string(), cur_box.clone())); } } } // Exit box when closing brace reduces depth below box_depth let post_brace = pre_brace + opens - closes; if post_brace < box_depth { in_box = false; cur_box.clear(); } } // Update brace after processing brace += opens - closes; } if violations.is_empty() { return Ok(()); } if strict { // Compose error message let mut msg = String::from("Field declarations must appear at the top of box. Violations:\n"); for (lno, fld, bx) in violations.iter().take(10) { msg.push_str(&format!( " line {} in box {}: '{}", lno, if bx.is_empty() { "" } else { bx }, fld )); msg.push_str("'\n"); } if violations.len() > 10 { msg.push_str(&format!(" ... and {} more\n", violations.len() - 10)); } return Err(msg); } if verbose || crate::config::env::env_bool("NYASH_RESOLVE_TRACE") { for (lno, fld, bx) in violations { eprintln!( "[lint] fields-top: line {} in box {} -> {}", lno, if bx.is_empty() { "" } else { &bx }, fld ); } } Ok(()) } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; #[test] fn plugin_meta_requires_prefix_even_when_relaxed() { let dir = tempdir().expect("tempdir"); let old = std::env::current_dir().expect("cwd"); std::env::set_current_dir(dir.path()).expect("chdir"); let toml = r#" [plugins] require_prefix = false [plugins."test-plugin"] prefix = "test" require_prefix = true expose_short_names = false boxes = ["ArrayBox"] "#; std::fs::write("nyash.toml", toml).expect("write nyash.toml"); crate::runner::box_index::refresh_box_index(); crate::runner::box_index::cache_clear(); let res = resolve_using_target( "ArrayBox", false, &[], &[], &HashMap::new(), &std::collections::HashMap::::new(), None, false, false, ); assert!(res.is_err(), "expected prefix enforcement"); let err = res.err().unwrap(); assert!(err.contains("requires prefix")); assert!(err.contains("test.")); std::env::set_current_dir(old).expect("restore cwd"); crate::runner::box_index::refresh_box_index(); crate::runner::box_index::cache_clear(); } }