Repository: casey/just
File: src/loader.rs (Rust)

Code before:
    let src = self.perform_load(path, HashSet::new())?;
    Ok(self.arena.alloc(src))
  }

  fn perform_load(&self, path: &Path, seen_paths: HashSet<PathBuf>) -> RunResult<String> {
    let src = fs::read_to_string(path).map_err(|io_error| Error::Load {
      path: path.to_owned(),
      io_error,
    })?;

    self.process_includes(src, path, seen_paths)
  }

  /// Given the original contents of a Justfile (with include directives), load all the included
  /// paths to produce one String with the contents of all the files concatenated.
  ///
  /// Note that this does not do any parsing yet (i.e. nothing stops a justfile from including a
  /// file that is not a valid justfile), and that (currently) line numbers in error messages will
  /// reference lines in this concatenated String rather than probably-more-useful information
  /// about the original file an error came from.
  ///
  /// !include directives are allowed to appear in a justfile only before the first non-blank,
  /// non-comment line. This is because this processing step occurs before parsing, and we don't
  /// want to accidentally process the string "!include" when it happens to occur within a string
  /// or in another post-parsing syntactic context where it shouldn't be.
  fn process_includes(
    &self,
    original: String,
    current_justfile_path: &Path,
    seen_paths: HashSet<PathBuf>,
  ) -> RunResult<String> {
    let has_final_newline = original.ends_with('\n');

    let include_regexp = Regex::new(INCLUDE_REGEX).unwrap();

    //NOTE this string-processing code can be made a bit cleaner once the Rust std lib Intersperse
    //API is stabilized (https://doc.rust-lang.org/std/iter/struct.Intersperse.html)

    let mut buf = String::new();
    let mut lines = original.lines().enumerate().peekable();

    let mut seen_first_contentful_line = false;

    while let Some((line_num, line)) = lines.next() {
      match include_regexp.captures(line) {
        Some(_) if seen_first_contentful_line => {
          return Err(Error::InvalidInclude {
            line_number: line_num + 1,
          });
        }
        Some(captures) => {

Review comment (suggestion):
This is only called at the end of `load_with_includes_recursive`, which is itself short, so I'd just merge these two functions.

Code after:
pub(crate) struct Loader {
  arena: Arena<String>,
  unstable: bool,
}

impl Loader {
  pub(crate) fn new(unstable: bool) -> Self {
    Loader {
      arena: Arena::new(),
      unstable,
    }
  }

  pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
    let src = self.load_recursive(path, HashSet::new())?;
    Ok(self.arena.alloc(src))
  }

  fn load_file<'a>(path: &Path) -> RunResult<'a, String> {
    fs::read_to_string(path).map_err(|io_error| Error::Load {
      path: path.to_owned(),
      io_error,
    })
  }

  fn load_recursive(&self, file: &Path, seen: HashSet<PathBuf>) -> RunResult<String> {
    let src = Self::load_file(file)?;

    let mut output = String::new();

    let mut seen_content = false;

    for (i, line) in LinesWithEndings::new(&src).enumerate() {
      if !seen_content && line.starts_with('!') {
        let include = line
          .strip_prefix("!include")
          .ok_or_else(|| Error::InvalidDirective { line: line.into() })?;

        if !self.unstable {
          return Err(Error::Unstable {
            message: "The !include directive is currently unstable.".into(),
          });
        }

        let argument = include.trim();

        if argument.is_empty() {
          return Err(Error::IncludeMissingPath {
            file: file.to_owned(),
            line: i,
          });