Fix circular source causing Nushell to crash (#12262)

# Description

EngineState now tracks the script currently running, instead of the
parent directory of the script. This also provides an easy way to expose
the current running script to the user (Issue #12195).

Similarly, StateWorkingSet now tracks scripts instead of directories.
`parsed_module_files` and `currently_parsed_pwd` are merged into one
variable, `scripts`, which acts like a stack for tracking the current
running script (which is on the top of the stack).

Circular import check is added for `source` operations, in addition to
module import. A simple testcase is added for circular source.

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->


<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

It shouldn't have any user facing changes.
This commit is contained in:
YizhePKU
2024-04-19 14:38:08 +08:00
committed by GitHub
parent 351bff8233
commit 6d2cb4382a
14 changed files with 164 additions and 121 deletions

View File

@ -98,8 +98,8 @@ pub struct EngineState {
config_path: HashMap<String, PathBuf>,
pub history_enabled: bool,
pub history_session_id: i64,
// If Nushell was started, e.g., with `nu spam.nu`, the file's parent is stored here
pub currently_parsed_cwd: Option<PathBuf>,
// Path to the file Nushell is currently evaluating, or None if we're in an interactive session.
pub file: Option<PathBuf>,
pub regex_cache: Arc<Mutex<LruCache<String, Regex>>>,
pub is_interactive: bool,
pub is_login: bool,
@ -161,7 +161,7 @@ impl EngineState {
config_path: HashMap::new(),
history_enabled: true,
history_session_id: 0,
currently_parsed_cwd: None,
file: None,
regex_cache: Arc::new(Mutex::new(LruCache::new(
NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
))),
@ -322,15 +322,6 @@ impl EngineState {
Ok(())
}
/// Mark a starting point if it is a script (e.g., nu spam.nu)
pub fn start_in_file(&mut self, file_path: Option<&str>) {
self.currently_parsed_cwd = if let Some(path) = file_path {
Path::new(path).parent().map(PathBuf::from)
} else {
None
};
}
pub fn has_overlay(&self, name: &[u8]) -> bool {
self.scope
.overlays

View File

@ -10,7 +10,7 @@ use crate::{
use core::panic;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
path::{Path, PathBuf},
sync::Arc,
};
@ -26,10 +26,7 @@ pub struct StateWorkingSet<'a> {
pub permanent_state: &'a EngineState,
pub delta: StateDelta,
pub external_commands: Vec<Vec<u8>>,
/// Current working directory relative to the file being parsed right now
pub currently_parsed_cwd: Option<PathBuf>,
/// All previously parsed module files. Used to protect against circular imports.
pub parsed_module_files: Vec<PathBuf>,
pub files: FileStack,
/// Whether or not predeclarations are searched when looking up a command (used with aliases)
pub search_predecls: bool,
pub parse_errors: Vec<ParseError>,
@ -38,12 +35,18 @@ pub struct StateWorkingSet<'a> {
impl<'a> StateWorkingSet<'a> {
pub fn new(permanent_state: &'a EngineState) -> Self {
// Initialize the file stack with the top-level file.
let files = if let Some(file) = permanent_state.file.clone() {
FileStack::with_file(file)
} else {
FileStack::new()
};
Self {
delta: StateDelta::new(permanent_state),
permanent_state,
external_commands: vec![],
currently_parsed_cwd: permanent_state.currently_parsed_cwd.clone(),
parsed_module_files: vec![],
files,
search_predecls: true,
parse_errors: vec![],
parse_warnings: vec![],
@ -1100,3 +1103,65 @@ impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
Err(miette::MietteError::OutOfBounds)
}
}
/// Files being evaluated, arranged as a stack.
///
/// The current active file is on the top of the stack.
/// When a file source/import another file, the new file is pushed onto the stack.
/// Attempting to add files that are already in the stack (circular import) results in an error.
///
/// Note that file paths are compared without canonicalization, so the same
/// physical file may still appear multiple times under different paths.
/// This doesn't affect circular import detection though.
#[derive(Debug, Default)]
pub struct FileStack(Vec<PathBuf>);
impl FileStack {
/// Creates an empty stack.
pub fn new() -> Self {
Self(vec![])
}
/// Creates a stack with a single file on top.
///
/// This is a convenience method that creates an empty stack, then pushes the file onto it.
/// It skips the circular import check and always succeeds.
pub fn with_file(path: PathBuf) -> Self {
Self(vec![path])
}
/// Adds a file to the stack.
///
/// If the same file is already present in the stack, returns `ParseError::CircularImport`.
pub fn push(&mut self, path: PathBuf, span: Span) -> Result<(), ParseError> {
// Check for circular import.
if let Some(i) = self.0.iter().rposition(|p| p == &path) {
let filenames: Vec<String> = self.0[i..]
.iter()
.chain(std::iter::once(&path))
.map(|p| p.to_string_lossy().to_string())
.collect();
let msg = filenames.join("\nuses ");
return Err(ParseError::CircularImport(msg, span));
}
self.0.push(path);
Ok(())
}
/// Removes a file from the stack and returns its path, or None if the stack is empty.
pub fn pop(&mut self) -> Option<PathBuf> {
self.0.pop()
}
/// Returns the active file (that is, the file on the top of the stack), or None if the stack is empty.
pub fn top(&self) -> Option<&Path> {
self.0.last().map(PathBuf::as_path)
}
/// Returns the parent directory of the active file, or None if the stack is empty
/// or the active file doesn't have a parent directory as part of its path.
pub fn current_working_directory(&self) -> Option<&Path> {
self.0.last().and_then(|path| path.parent())
}
}

View File

@ -227,9 +227,9 @@ pub enum ParseError {
#[label = "module directory is missing a mod.nu file"] Span,
),
#[error("Cyclical module import.")]
#[diagnostic(code(nu::parser::cyclical_module_import), help("{0}"))]
CyclicalModuleImport(String, #[label = "detected cyclical module import"] Span),
#[error("Circular import.")]
#[diagnostic(code(nu::parser::circular_import), help("{0}"))]
CircularImport(String, #[label = "detected circular import"] Span),
#[error("Can't export {0} named same as the module.")]
#[diagnostic(
@ -506,7 +506,7 @@ impl ParseError {
ParseError::NamedAsModule(_, _, _, s) => *s,
ParseError::ModuleDoubleMain(_, s) => *s,
ParseError::ExportMainAliasNotAllowed(s) => *s,
ParseError::CyclicalModuleImport(_, s) => *s,
ParseError::CircularImport(_, s) => *s,
ParseError::ModuleOrOverlayNotFound(s) => *s,
ParseError::ActiveOverlayNotFound(s) => *s,
ParseError::OverlayPrefixMismatch(_, _, s) => *s,

View File

@ -11,6 +11,7 @@ use std::{
path::{Path, PathBuf},
};
/// Create a Value for `$nu`.
pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Value, ShellError> {
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
let cwd = engine_state.current_work_dir();