Skip to content

Commit

Permalink
Switch to closest file configuration resolution (#916)
Browse files Browse the repository at this point in the history
* Closest file config resolution

* Respect config path override for stdin filepath
  • Loading branch information
JohnnyMorganz authored Nov 17, 2024
1 parent 5972683 commit d11797b
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 163 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Update internal Lua parser version (full-moon) to v1.1.0. This includes parser performance improvements. ([#854](https://github.com/JohnnyMorganz/StyLua/issues/854))
- LuaJIT is now separated from Lua52, and is available in its own feature and syntax flag
- `.stylua.toml` config resolution now supports looking up config files next to files being formatted, recursively going
upwards until reaching the current working directory, then stopping (unless `--search-parent-directories` was specified).
For example, for a file `./src/test.lua`, executing `stylua src/` will look for `./src/stylua.toml` and then `./stylua.toml`.

### Fixed

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ StyLua has opinionated defaults, but also provides a few options that can be set

### Finding the configuration

The CLI looks for `stylua.toml` or `.stylua.toml` in the directory where the tool was executed.
The CLI looks for a `stylua.toml` or `.stylua.toml` starting from the directory of the file being formatted.
It will keep searching upwards until it reaches the current directory where the tool was executed.
If not found, we search for an `.editorconfig` file, otherwise fall back to the default configuration.
This feature can be disabled using `--no-editorconfig`.
See [EditorConfig](https://editorconfig.org/) for more details.
Expand Down
283 changes: 180 additions & 103 deletions src/cli/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crate::opt::Opt;
use anyhow::{Context, Result};
use log::*;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use stylua_lib::Config;
use stylua_lib::SortRequiresConfig;

#[cfg(feature = "editorconfig")]
use stylua_lib::editorconfig;

static CONFIG_FILE_NAME: [&str; 2] = ["stylua.toml", ".stylua.toml"];

fn read_config_file(path: &Path) -> Result<Config> {
Expand All @@ -16,144 +20,217 @@ fn read_config_file(path: &Path) -> Result<Config> {
Ok(config)
}

/// Searches the directory for the configuration toml file (i.e. `stylua.toml` or `.stylua.toml`)
fn find_toml_file(directory: &Path) -> Option<PathBuf> {
for name in &CONFIG_FILE_NAME {
let file_path = directory.join(name);
if file_path.exists() {
return Some(file_path);
}
}
fn read_and_apply_overrides(path: &Path, opt: &Opt) -> Result<Config> {
read_config_file(path).map(|config| load_overrides(config, opt))
}

None
pub struct ConfigResolver<'a> {
config_cache: HashMap<PathBuf, Option<Config>>,
forced_configuration: Option<Config>,
current_directory: PathBuf,
default_configuration: Config,
opt: &'a Opt,
}

/// Looks for a configuration file in the directory provided (and its parent's recursively, if specified)
fn find_config_file(mut directory: PathBuf, recursive: bool) -> Result<Option<Config>> {
debug!("config: looking for config in {}", directory.display());
let config_file = find_toml_file(&directory);
match config_file {
Some(file_path) => {
debug!("config: found config at {}", file_path.display());
read_config_file(&file_path).map(Some)
impl ConfigResolver<'_> {
pub fn new(opt: &Opt) -> Result<ConfigResolver> {
let forced_configuration = opt
.config_path
.as_ref()
.map(|config_path| {
debug!(
"config: explicit config path provided at {}",
config_path.display()
);
read_and_apply_overrides(config_path, opt)
})
.transpose()?;

Ok(ConfigResolver {
config_cache: HashMap::new(),
forced_configuration,
current_directory: env::current_dir().context("Could not find current directory")?,
default_configuration: load_overrides(Config::default(), opt),
opt,
})
}

pub fn load_configuration(&mut self, path: &Path) -> Result<Config> {
if let Some(configuration) = self.forced_configuration {
return Ok(configuration);
}
None => {
// Both don't exist, search up the tree if necessary
// directory.pop() mutates the path to get its parent, and returns false if no more parent
if recursive && directory.pop() {
find_config_file(directory, recursive)
} else {
Ok(None)

let root = match self.opt.search_parent_directories {
true => None,
false => Some(self.current_directory.to_path_buf()),
};

let absolute_path = self.current_directory.join(path);
let parent_path = &absolute_path
.parent()
.with_context(|| format!("no parent directory found for {}", path.display()))?;

match self.find_config_file(parent_path, root)? {
Some(config) => Ok(config),
None => {
#[cfg(feature = "editorconfig")]
if self.opt.no_editorconfig {
Ok(self.default_configuration)
} else {
editorconfig::parse(self.default_configuration, path)
.context("could not parse editorconfig")
}
#[cfg(not(feature = "editorconfig"))]
Ok(self.default_configuration)
}
}
}
}

pub fn find_ignore_file_path(mut directory: PathBuf, recursive: bool) -> Option<PathBuf> {
debug!("config: looking for ignore file in {}", directory.display());
let file_path = directory.join(".styluaignore");
if file_path.is_file() {
Some(file_path)
} else if recursive && directory.pop() {
find_ignore_file_path(directory, recursive)
} else {
None
}
}
pub fn load_configuration_for_stdin(&mut self) -> Result<Config> {
if let Some(configuration) = self.forced_configuration {
return Ok(configuration);
}

/// Looks for a configuration file at either `$XDG_CONFIG_HOME`, `$XDG_CONFIG_HOME/stylua`, `$HOME/.config` or `$HOME/.config/stylua`
fn search_config_locations() -> Result<Option<Config>> {
// Look in `$XDG_CONFIG_HOME`
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
let xdg_config_path = Path::new(&xdg_config);
if xdg_config_path.exists() {
debug!("config: looking in $XDG_CONFIG_HOME");
match &self.opt.stdin_filepath {
Some(filepath) => self.load_configuration(filepath),
None => {
#[cfg(feature = "editorconfig")]
if self.opt.no_editorconfig {
Ok(self.default_configuration)
} else {
editorconfig::parse(self.default_configuration, &PathBuf::from("*.lua"))
.context("could not parse editorconfig")
}
#[cfg(not(feature = "editorconfig"))]
Ok(self.default_configuration)
}
}
}

if let Some(config) = find_config_file(xdg_config_path.to_path_buf(), false)? {
return Ok(Some(config));
fn lookup_config_file_in_directory(&self, directory: &Path) -> Result<Option<Config>> {
debug!("config: looking for config in {}", directory.display());
let config_file = find_toml_file(directory);
match config_file {
Some(file_path) => {
debug!("config: found config at {}", file_path.display());
read_config_file(&file_path).map(Some)
}
None => Ok(None),
}
}

debug!("config: looking in $XDG_CONFIG_HOME/stylua");
let xdg_config_path = xdg_config_path.join("stylua");
if xdg_config_path.exists() {
if let Some(config) = find_config_file(xdg_config_path, false)? {
return Ok(Some(config));
/// Looks for a configuration file in the directory provided
/// Keep searching recursively upwards until we hit the root (if provided), then stop
/// When `--search-parent-directories` is enabled, root = None, else root = Some(cwd)
fn find_config_file(
&mut self,
directory: &Path,
root: Option<PathBuf>,
) -> Result<Option<Config>> {
if let Some(config) = self.config_cache.get(directory) {
return Ok(*config);
}

let resolved_configuration = match self.lookup_config_file_in_directory(directory)? {
Some(config) => Some(config),
None => {
let parent_directory = directory.parent();
let should_stop = Some(directory) == root.as_deref() || parent_directory.is_none();

if should_stop {
debug!("config: no configuration file found");
if self.opt.search_parent_directories {
if let Some(config) = self.search_config_locations()? {
return Ok(Some(config));
}
}

debug!("config: falling back to default config");
None
} else {
self.find_config_file(parent_directory.unwrap(), root)?
}
}
}
};

self.config_cache
.insert(directory.to_path_buf(), resolved_configuration);
Ok(resolved_configuration)
}

// Look in `$HOME/.config`
if let Ok(home) = std::env::var("HOME") {
let home_config_path = Path::new(&home).join(".config");
/// Looks for a configuration file at either `$XDG_CONFIG_HOME`, `$XDG_CONFIG_HOME/stylua`, `$HOME/.config` or `$HOME/.config/stylua`
fn search_config_locations(&self) -> Result<Option<Config>> {
// Look in `$XDG_CONFIG_HOME`
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
let xdg_config_path = Path::new(&xdg_config);
if xdg_config_path.exists() {
debug!("config: looking in $XDG_CONFIG_HOME");

if home_config_path.exists() {
debug!("config: looking in $HOME/.config");
if let Some(config) = self.lookup_config_file_in_directory(xdg_config_path)? {
return Ok(Some(config));
}

if let Some(config) = find_config_file(home_config_path.to_owned(), false)? {
return Ok(Some(config));
debug!("config: looking in $XDG_CONFIG_HOME/stylua");
let xdg_config_path = xdg_config_path.join("stylua");
if xdg_config_path.exists() {
if let Some(config) = self.lookup_config_file_in_directory(&xdg_config_path)? {
return Ok(Some(config));
}
}
}
}

// Look in `$HOME/.config`
if let Ok(home) = std::env::var("HOME") {
let home_config_path = Path::new(&home).join(".config");

debug!("config: looking in $HOME/.config/stylua");
let home_config_path = home_config_path.join("stylua");
if home_config_path.exists() {
if let Some(config) = find_config_file(home_config_path, false)? {
debug!("config: looking in $HOME/.config");

if let Some(config) = self.lookup_config_file_in_directory(&home_config_path)? {
return Ok(Some(config));
}

debug!("config: looking in $HOME/.config/stylua");
let home_config_path = home_config_path.join("stylua");
if home_config_path.exists() {
if let Some(config) = self.lookup_config_file_in_directory(&home_config_path)? {
return Ok(Some(config));
}
}
}
}
}

Ok(None)
Ok(None)
}
}

pub fn load_config(opt: &Opt) -> Result<Option<Config>> {
match &opt.config_path {
Some(config_path) => {
debug!(
"config: explicit config path provided at {}",
config_path.display()
);
read_config_file(config_path).map(Some)
/// Searches the directory for the configuration toml file (i.e. `stylua.toml` or `.stylua.toml`)
fn find_toml_file(directory: &Path) -> Option<PathBuf> {
for name in &CONFIG_FILE_NAME {
let file_path = directory.join(name);
if file_path.exists() {
return Some(file_path);
}
None => {
let current_dir = match &opt.stdin_filepath {
Some(file_path) => file_path
.parent()
.context("Could not find current directory from provided stdin filepath")?
.to_path_buf(),
None => env::current_dir().context("Could not find current directory")?,
};

debug!(
"config: starting config search from {} - recursively searching parents: {}",
current_dir.display(),
opt.search_parent_directories
);
let config = find_config_file(current_dir, opt.search_parent_directories)?;
match config {
Some(config) => Ok(Some(config)),
None => {
debug!("config: no configuration file found");
}

// Search the configuration directory for a file, if necessary
if opt.search_parent_directories {
if let Some(config) = search_config_locations()? {
return Ok(Some(config));
}
}
None
}

// Fallback to a default configuration
debug!("config: falling back to default config");
Ok(None)
}
}
}
pub fn find_ignore_file_path(mut directory: PathBuf, recursive: bool) -> Option<PathBuf> {
debug!("config: looking for ignore file in {}", directory.display());
let file_path = directory.join(".styluaignore");
if file_path.is_file() {
Some(file_path)
} else if recursive && directory.pop() {
find_ignore_file_path(directory, recursive)
} else {
None
}
}

/// Handles any overrides provided by command line options
pub fn load_overrides(config: Config, opt: &Opt) -> Config {
fn load_overrides(config: Config, opt: &Opt) -> Config {
let mut new_config = config;

if let Some(syntax) = opt.format_opts.syntax {
Expand Down
Loading

0 comments on commit d11797b

Please sign in to comment.