From 734c909f0816e3a53cbbf799ca5bf4d7c110b6f2 Mon Sep 17 00:00:00 2001 From: Gary Miguel Date: Wed, 18 Feb 2026 19:46:23 +0000 Subject: [PATCH 1/3] feat: per-file config resolution for multi-directory linting When linting files across multiple directories, each file now uses the nearest .sqruff/.sqlfluff config found in its ancestor directories. This fixes two issues: 1. Running sqruff from a parent directory with different configs in subdirectories now applies the correct config per file. 2. Running sqruff from a subdirectory finds config in ancestor dirs. CLI --dialect override is applied on top of any per-file config. --- crates/cli-lib/src/commands_fix.rs | 9 +- crates/cli-lib/src/commands_lint.rs | 7 +- crates/cli-lib/src/lib.rs | 76 ++++++++++++----- crates/lib/src/core/config.rs | 125 ++++++++++++++++----------- crates/lib/src/core/linter/core.rs | 127 ++++++++++++++++++++++------ 5 files changed, 243 insertions(+), 101 deletions(-) diff --git a/crates/cli-lib/src/commands_fix.rs b/crates/cli-lib/src/commands_fix.rs index 365501660..17f1d6334 100644 --- a/crates/cli-lib/src/commands_fix.rs +++ b/crates/cli-lib/src/commands_fix.rs @@ -2,6 +2,7 @@ use crate::commands::FixArgs; use crate::commands::Format; use crate::linter; use sqruff_lib::core::config::FluffConfig; +use sqruff_lib_core::dialects::init::DialectKind; use std::path::Path; pub(crate) fn run_fix( @@ -9,9 +10,10 @@ pub(crate) fn run_fix( config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, + dialect_override: Option, ) -> i32 { let FixArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors); + let mut linter = linter(config, format, collect_parse_errors, dialect_override); let result = match linter.lint_paths(paths, true, &ignorer) { Ok(result) => result, Err(e) => { @@ -46,10 +48,11 @@ pub(crate) fn run_fix_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, + dialect_override: Option, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors); + let linter = linter(config, format, collect_parse_errors, dialect_override); let result = match linter.lint_string(&read_in, None, true) { Ok(result) => result, Err(e) => { @@ -95,7 +98,7 @@ mod tests { format: Format::Human, }; let config = FluffConfig::default(); - run_fix(args, config, ignore_none, true); + run_fix(args, config, ignore_none, true, None); let after = std::fs::metadata(&path).unwrap().modified().unwrap(); assert_eq!(before, after); diff --git a/crates/cli-lib/src/commands_lint.rs b/crates/cli-lib/src/commands_lint.rs index ba788898a..16583b7f1 100644 --- a/crates/cli-lib/src/commands_lint.rs +++ b/crates/cli-lib/src/commands_lint.rs @@ -1,6 +1,7 @@ use crate::commands::{Format, LintArgs}; use crate::linter; use sqruff_lib::core::config::FluffConfig; +use sqruff_lib_core::dialects::init::DialectKind; use std::path::Path; pub(crate) fn run_lint( @@ -8,9 +9,10 @@ pub(crate) fn run_lint( config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, + dialect_override: Option, ) -> i32 { let LintArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors); + let mut linter = linter(config, format, collect_parse_errors, dialect_override); let result = match linter.lint_paths(paths, false, &ignorer) { Ok(result) => result, Err(e) => { @@ -28,10 +30,11 @@ pub(crate) fn run_lint_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, + dialect_override: Option, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors); + let linter = linter(config, format, collect_parse_errors, dialect_override); let result = match linter.lint_string(&read_in, None, false) { Ok(result) => result, Err(e) => { diff --git a/crates/cli-lib/src/lib.rs b/crates/cli-lib/src/lib.rs index 5cb8dd3db..93c4def70 100644 --- a/crates/cli-lib/src/lib.rs +++ b/crates/cli-lib/src/lib.rs @@ -46,7 +46,7 @@ where let cli = Cli::parse_from(args); let collect_parse_errors = cli.parsing_errors; - let mut config: FluffConfig = if let Some(config) = cli.config.as_ref() { + let config: FluffConfig = if let Some(config) = cli.config.as_ref() { if !Path::new(config).is_file() { eprintln!( "The specified config file '{}' does not exist.", @@ -57,24 +57,19 @@ where }; FluffConfig::from_file(Path::new(config)) } else { + // Load a base config from cwd ancestors. Per-file config resolution + // happens inside the Linter during lint_paths. FluffConfig::from_root(None, false, None).unwrap() }; - if let Some(dialect) = cli.dialect { - let dialect_kind = DialectKind::try_from(dialect.as_str()); - match dialect_kind { - Ok(dialect_kind) => { - config.override_dialect(dialect_kind).unwrap_or_else(|e| { - eprintln!("{}", e); - std::process::exit(1); - }); - } - Err(e) => { - eprintln!("{}", e); - std::process::exit(1); - } - } - } + // Parse dialect override from CLI; it will be applied per-file in the + // linter, taking priority over any .sqruff config. + let dialect_override: Option = cli.dialect.map(|dialect| { + DialectKind::try_from(dialect.as_str()).unwrap_or_else(|e| { + eprintln!("{}", e); + std::process::exit(1); + }) + }); let current_path = std::env::current_dir().unwrap(); let ignore_file = ignore::IgnoreFile::new_from_root(¤t_path).unwrap(); @@ -90,16 +85,38 @@ where eprintln!("{e}"); 1 } - Ok(false) => commands_lint::run_lint(args, config, ignorer, collect_parse_errors), - Ok(true) => commands_lint::run_lint_stdin(config, args.format, collect_parse_errors), + Ok(false) => commands_lint::run_lint( + args, + config, + ignorer, + collect_parse_errors, + dialect_override, + ), + Ok(true) => commands_lint::run_lint_stdin( + config, + args.format, + collect_parse_errors, + dialect_override, + ), }, Commands::Fix(args) => match is_std_in_flag_input(&args.paths) { Err(e) => { eprintln!("{e}"); 1 } - Ok(false) => commands_fix::run_fix(args, config, ignorer, collect_parse_errors), - Ok(true) => commands_fix::run_fix_stdin(config, args.format, collect_parse_errors), + Ok(false) => commands_fix::run_fix( + args, + config, + ignorer, + collect_parse_errors, + dialect_override, + ), + Ok(true) => commands_fix::run_fix_stdin( + config, + args.format, + collect_parse_errors, + dialect_override, + ), }, Commands::Lsp => { sqruff_lsp::run(); @@ -126,7 +143,12 @@ where } } -pub(crate) fn linter(config: FluffConfig, format: Format, collect_parse_errors: bool) -> Linter { +pub(crate) fn linter( + config: FluffConfig, + format: Format, + collect_parse_errors: bool, + dialect_override: Option, +) -> Linter { let formatter: Arc = match format { Format::Human => { let output_stream = std::io::stderr().into(); @@ -148,5 +170,15 @@ pub(crate) fn linter(config: FluffConfig, format: Format, collect_parse_errors: } }; - Linter::new(config, Some(formatter), None, collect_parse_errors) + let mut config = config; + if let Some(dialect) = dialect_override { + config + .override_dialect(dialect) + .expect("invalid dialect override"); + } + let mut linter = Linter::new(config, Some(formatter), None, collect_parse_errors); + if let Some(dialect) = dialect_override { + linter.set_dialect_override(dialect); + } + linter } diff --git a/crates/lib/src/core/config.rs b/crates/lib/src/core/config.rs index 31f985b12..a4d6d0392 100644 --- a/crates/lib/src/core/config.rs +++ b/crates/lib/src/core/config.rs @@ -192,10 +192,21 @@ impl FluffConfig { extra_config_path: Option, ignore_local_config: bool, overrides: Option>, + ) -> Result { + Self::from_path(Path::new("."), extra_config_path, ignore_local_config, overrides) + } + + /// Loads config by searching for .sqruff/.sqlfluff files in ancestor + /// directories of `path`, with closer configs taking precedence. + pub fn from_path( + path: &Path, + extra_config_path: Option, + ignore_local_config: bool, + overrides: Option>, ) -> Result { let loader = ConfigLoader {}; let mut config = - loader.load_config_up_to_path(".", extra_config_path.clone(), ignore_local_config); + loader.load_config_up_to_path(path, extra_config_path.clone(), ignore_local_config); if let Some(overrides) = overrides && let Some(dialect) = overrides.get("dialect") @@ -284,6 +295,29 @@ impl Default for FluffConfigIndentation { pub struct ConfigLoader; impl ConfigLoader { + /// Search ancestor directories of `path` for the nearest directory + /// containing a `.sqruff` or `.sqlfluff` config file. Returns the + /// directory path if found. + pub fn find_nearest_config_dir(path: &Path) -> Option { + let mut dir = if path.is_file() { + path.parent()?.to_path_buf() + } else { + path.to_path_buf() + }; + dir = std::path::absolute(&dir).ok()?; + + loop { + for fname in [".sqruff", ".sqlfluff"] { + if dir.join(fname).exists() { + return Some(dir); + } + } + if !dir.pop() { + return None; + } + } + } + #[allow(unused_variables)] fn iter_config_locations_up_to_path( path: &Path, @@ -291,60 +325,16 @@ impl ConfigLoader { ignore_local_config: bool, ) -> impl Iterator { let mut given_path = std::path::absolute(path).unwrap(); - let working_path = std::env::current_dir().unwrap(); if !given_path.is_dir() { given_path = given_path.parent().unwrap().into(); } - let common_path = common_path::common_path(&given_path, working_path).unwrap(); - let mut path_to_visit = common_path; - - let head = Some(given_path.canonicalize().unwrap()).into_iter(); - let tail = std::iter::from_fn(move || { - if path_to_visit != given_path { - let path = path_to_visit.canonicalize().unwrap(); - - let next_path_to_visit = { - // Convert `path_to_visit` & `given_path` to `Path` - let path_to_visit_as_path = path_to_visit.as_path(); - let given_path_as_path = given_path.as_path(); - - // Attempt to create a relative path from `given_path` to `path_to_visit` - match given_path_as_path.strip_prefix(path_to_visit_as_path) { - Ok(relative_path) => { - // Get the first component of the relative path - if let Some(first_part) = relative_path.components().next() { - // Combine `path_to_visit` with the first part of the relative path - path_to_visit.join(first_part.as_os_str()) - } else { - // If there are no components in the relative path, return - // `path_to_visit` - path_to_visit.clone() - } - } - Err(_) => { - // If `given_path` is not relative to `path_to_visit`, handle the error - // (e.g., return `path_to_visit`) - // This part depends on how you want to handle the error. - path_to_visit.clone() - } - } - }; - - if next_path_to_visit == path_to_visit { - return None; - } - - path_to_visit = next_path_to_visit; - - Some(path) - } else { - None - } - }); - - head.chain(tail) + // Collect ancestors from root to given_path (inclusive). + // nested_combine uses last-wins, so closest config takes precedence. + let mut ancestors: Vec = given_path.ancestors().map(|p| p.to_path_buf()).collect(); + ancestors.reverse(); + ancestors.into_iter() } pub fn load_config_up_to_path( @@ -563,6 +553,41 @@ dialect = bigquery assert_eq!(config.get_dialect().name, DialectKind::Bigquery); } + #[test] + fn test_find_nearest_config_dir() { + let base = std::env::temp_dir().join("sqruff_test_find_config"); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir_all(&base).unwrap(); + + // Create base/.sqruff + std::fs::write(base.join(".sqruff"), "[sqruff]\ndialect = bigquery\n").unwrap(); + + // Create base/child/grandchild/ + let grandchild = base.join("child").join("grandchild"); + std::fs::create_dir_all(&grandchild).unwrap(); + + // From grandchild, should find base + let found = ConfigLoader::find_nearest_config_dir(&grandchild); + assert_eq!(found.unwrap(), std::path::absolute(&base).unwrap()); + + // Create base/child/.sqruff + std::fs::write( + base.join("child").join(".sqruff"), + "[sqruff]\ndialect = ansi\n", + ) + .unwrap(); + + // Now from grandchild, should find child + let found = ConfigLoader::find_nearest_config_dir(&grandchild); + assert_eq!( + found.unwrap(), + std::path::absolute(base.join("child")).unwrap() + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&base); + } + #[test] fn test_dialect_without_config_section() { // Test that a dialect works without a config section diff --git a/crates/lib/src/core/linter/core.rs b/crates/lib/src/core/linter/core.rs index cf630b4d9..a46e823a4 100644 --- a/crates/lib/src/core/linter/core.rs +++ b/crates/lib/src/core/linter/core.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use crate::Formatter; -use crate::core::config::FluffConfig; +use crate::core::config::{ConfigLoader, FluffConfig}; use crate::core::linter::common::{ParsedString, RenderedFile}; use crate::core::linter::linted_file::LintedFile; use crate::core::linter::linting_result::LintingResult; @@ -19,6 +19,7 @@ use itertools::Itertools; use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; use smol_str::{SmolStr, ToSmolStr}; use sqruff_lib_core::dialects::Dialect; +use sqruff_lib_core::dialects::init::DialectKind; use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet}; use sqruff_lib_core::errors::{ SQLBaseError, SQLFluffUserError, SQLLexError, SQLLintError, SQLParseError, @@ -38,6 +39,9 @@ pub struct Linter { /// include_parse_errors is a flag to indicate whether to include parse errors in the output include_parse_errors: bool, + + /// CLI dialect override, applied on top of any per-file config. + dialect_override: Option, } impl Linter { @@ -57,6 +61,7 @@ impl Linter { templater, rules: OnceLock::new(), include_parse_errors, + dialect_override: None, } } @@ -71,6 +76,22 @@ impl Linter { } } + pub fn set_dialect_override(&mut self, dialect: DialectKind) { + self.dialect_override = Some(dialect); + } + + /// Update the linter's config, resetting cached templater and rules. + fn set_config(&mut self, mut config: FluffConfig) { + if let Some(dialect) = self.dialect_override { + config + .override_dialect(dialect) + .expect("invalid dialect override"); + } + self.templater = Self::get_templater(&config); + self.rules = OnceLock::new(); + self.config = config; + } + /// Lint strings directly. pub fn lint_string_wrapped( &mut self, @@ -154,33 +175,48 @@ impl Linter { }) .collect_vec(); + // Group files by their nearest config directory so each group uses the + // right .sqruff/.sqlfluff config. + let mut groups: HashMap, Vec> = HashMap::new(); + for path in &paths { + let config_dir = ConfigLoader::find_nearest_config_dir(Path::new(path)); + groups.entry(config_dir).or_default().push(path.clone()); + } + let mut files = Vec::with_capacity(paths.len()); - match self.templater.processing_mode() { - ProcessingMode::Parallel => { - let results: Vec<_> = paths - .par_iter() - .map(|path| { - let rendered = self.render_file(path.clone()); - self.lint_rendered(rendered, fix) - }) - .collect(); - for result in results { - files.push(result?); - } + for (config_dir, group_paths) in groups { + // Load config for this group. + if let Some(dir) = &config_dir { + let config = FluffConfig::from_path(dir, None, false, None) + .unwrap_or_else(|_| FluffConfig::default()); + self.set_config(config); } - ProcessingMode::Batch => { - // Use batch processing for templaters that support it (e.g., dbt). - // This allows sharing expensive initialization (manifest loading) across files. - let rendered_files = self.render_files_batch(&paths); - for rendered in rendered_files { - files.push(self.lint_rendered(rendered, fix)?); + + match self.templater.processing_mode() { + ProcessingMode::Parallel => { + let results: Vec<_> = group_paths + .par_iter() + .map(|path| { + let rendered = self.render_file(path.clone()); + self.lint_rendered(rendered, fix) + }) + .collect(); + for result in results { + files.push(result?); + } } - } - ProcessingMode::Sequential => { - for path in &paths { - let rendered = self.render_file(path.clone()); - files.push(self.lint_rendered(rendered, fix)?); + ProcessingMode::Batch => { + let rendered_files = self.render_files_batch(&group_paths); + for rendered in rendered_files { + files.push(self.lint_rendered(rendered, fix)?); + } + } + ProcessingMode::Sequential => { + for path in &group_paths { + let rendered = self.render_file(path.clone()); + files.push(self.lint_rendered(rendered, fix)?); + } } } } @@ -971,6 +1007,49 @@ mod tests { let _parsed = linter.parse_string(&tables, &sql, None).unwrap(); } + #[test] + fn test_lint_paths_per_file_config() { + let base = std::env::temp_dir().join("sqruff_test_per_file_config"); + let _ = std::fs::remove_dir_all(&base); + + // Create bq/ with bigquery config and a file using backtick-quoted identifiers + let bq_dir = base.join("bq").join("sub"); + std::fs::create_dir_all(&bq_dir).unwrap(); + std::fs::write( + base.join("bq").join(".sqruff"), + "[sqruff]\ndialect = bigquery\n", + ) + .unwrap(); + std::fs::write( + bq_dir.join("test.sql"), + "SELECT a FROM `p.d.t` WHERE a >= 1\n", + ) + .unwrap(); + + // Create ansi/ with no config (defaults to ansi) + let ansi_dir = base.join("ansi"); + std::fs::create_dir_all(&ansi_dir).unwrap(); + std::fs::write(ansi_dir.join("test.sql"), "SELECT a FROM t WHERE a >= 1\n").unwrap(); + + let config = FluffConfig::default(); + let mut linter = Linter::new(config, None, None, false); + + let result = linter + .lint_paths( + vec![bq_dir.join("test.sql"), ansi_dir.join("test.sql")], + false, + &|_| false, + ) + .unwrap(); + + // Both files should lint without panicking (the bigquery file would + // fail to parse under the ansi dialect due to backtick-quoted project + // identifiers). + assert_eq!(result.len(), 2); + + let _ = std::fs::remove_dir_all(&base); + } + #[test] fn test_normalise_newlines() { let in_str = "SELECT\r\n foo\n FROM \r \n\r bar;"; From ed90e30dc3e9018ebe377a05dac413e83ba3c247 Mon Sep 17 00:00:00 2001 From: Gary Miguel Date: Wed, 18 Feb 2026 20:00:31 +0000 Subject: [PATCH 2/3] feat: per-file config resolution for multi-directory linting When linting files across multiple directories, each file now uses the nearest .sqruff/.sqlfluff config found in its ancestor directories. This fixes two issues: 1. Running sqruff from a parent directory with different configs in subdirectories now applies the correct config per file. 2. Running sqruff from a subdirectory finds config in ancestor dirs. The Linter accepts optional CLI overrides (e.g. --dialect) at construction time and applies them on top of each per-file config via FluffConfig::from_path's existing overrides parameter. --- crates/cli-lib/src/commands_fix.rs | 10 ++--- crates/cli-lib/src/commands_lint.rs | 10 ++--- crates/cli-lib/src/commands_parse.rs | 4 +- crates/cli-lib/src/lib.rs | 46 +++++++++++------------ crates/cli/tests/ignore_data_directory.rs | 1 + crates/lib-wasm/src/lib.rs | 2 +- crates/lib/benches/depth_map.rs | 2 +- crates/lib/benches/fix.rs | 1 + crates/lib/src/core/linter/core.rs | 34 ++++++++--------- crates/lib/src/core/rules/noqa.rs | 4 ++ crates/lib/src/core/test_functions.rs | 2 +- crates/lib/src/rules/layout/lt05.rs | 2 +- crates/lib/src/templaters/placeholder.rs | 2 +- crates/lib/src/tests.rs | 3 ++ crates/lib/src/utils/reflow/reindent.rs | 2 +- crates/lib/tests/rules.rs | 8 ++-- crates/lsp/src/lib.rs | 2 +- 17 files changed, 71 insertions(+), 64 deletions(-) diff --git a/crates/cli-lib/src/commands_fix.rs b/crates/cli-lib/src/commands_fix.rs index 17f1d6334..fba4f1bae 100644 --- a/crates/cli-lib/src/commands_fix.rs +++ b/crates/cli-lib/src/commands_fix.rs @@ -2,7 +2,7 @@ use crate::commands::FixArgs; use crate::commands::Format; use crate::linter; use sqruff_lib::core::config::FluffConfig; -use sqruff_lib_core::dialects::init::DialectKind; +use std::collections::HashMap; use std::path::Path; pub(crate) fn run_fix( @@ -10,10 +10,10 @@ pub(crate) fn run_fix( config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, - dialect_override: Option, + cli_overrides: Option>, ) -> i32 { let FixArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors, dialect_override); + let mut linter = linter(config, format, collect_parse_errors, cli_overrides); let result = match linter.lint_paths(paths, true, &ignorer) { Ok(result) => result, Err(e) => { @@ -48,11 +48,11 @@ pub(crate) fn run_fix_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, - dialect_override: Option, + cli_overrides: Option>, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors, dialect_override); + let linter = linter(config, format, collect_parse_errors, cli_overrides); let result = match linter.lint_string(&read_in, None, true) { Ok(result) => result, Err(e) => { diff --git a/crates/cli-lib/src/commands_lint.rs b/crates/cli-lib/src/commands_lint.rs index 16583b7f1..1efaef760 100644 --- a/crates/cli-lib/src/commands_lint.rs +++ b/crates/cli-lib/src/commands_lint.rs @@ -1,7 +1,7 @@ use crate::commands::{Format, LintArgs}; use crate::linter; use sqruff_lib::core::config::FluffConfig; -use sqruff_lib_core::dialects::init::DialectKind; +use std::collections::HashMap; use std::path::Path; pub(crate) fn run_lint( @@ -9,10 +9,10 @@ pub(crate) fn run_lint( config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, - dialect_override: Option, + cli_overrides: Option>, ) -> i32 { let LintArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors, dialect_override); + let mut linter = linter(config, format, collect_parse_errors, cli_overrides); let result = match linter.lint_paths(paths, false, &ignorer) { Ok(result) => result, Err(e) => { @@ -30,11 +30,11 @@ pub(crate) fn run_lint_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, - dialect_override: Option, + cli_overrides: Option>, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors, dialect_override); + let linter = linter(config, format, collect_parse_errors, cli_overrides); let result = match linter.lint_string(&read_in, None, false) { Ok(result) => result, Err(e) => { diff --git a/crates/cli-lib/src/commands_parse.rs b/crates/cli-lib/src/commands_parse.rs index 764fa3d98..9474252ba 100644 --- a/crates/cli-lib/src/commands_parse.rs +++ b/crates/cli-lib/src/commands_parse.rs @@ -66,7 +66,7 @@ fn parse_and_output_tree( format: ParseFormat, ) -> i32 { // Create a linter and parse the SQL - let linter = Linter::new(config.clone(), None, None, true); + let linter = Linter::new(config.clone(), None, None, true, None); let tables = Tables::default(); match linter.parse_string(&tables, sql, Some(filename.to_string())) { @@ -74,7 +74,7 @@ fn parse_and_output_tree( if let Some(tree) = &parsed.tree { match format { ParseFormat::Json => { - let serialized = tree.to_serialised(false, true); + let serialized = tree.to_serialised(false, true, None); match serde_json::to_string_pretty(&serialized) { Ok(json) => println!("{}", json), Err(e) => { diff --git a/crates/cli-lib/src/lib.rs b/crates/cli-lib/src/lib.rs index 93c4def70..09b2bb70d 100644 --- a/crates/cli-lib/src/lib.rs +++ b/crates/cli-lib/src/lib.rs @@ -62,14 +62,16 @@ where FluffConfig::from_root(None, false, None).unwrap() }; - // Parse dialect override from CLI; it will be applied per-file in the - // linter, taking priority over any .sqruff config. - let dialect_override: Option = cli.dialect.map(|dialect| { - DialectKind::try_from(dialect.as_str()).unwrap_or_else(|e| { - eprintln!("{}", e); - std::process::exit(1); - }) - }); + // Build CLI overrides (e.g. --dialect) to apply on top of per-file configs. + let cli_overrides: Option> = + cli.dialect.map(|dialect| { + // Validate the dialect name early. + DialectKind::try_from(dialect.as_str()).unwrap_or_else(|e| { + eprintln!("{}", e); + std::process::exit(1); + }); + [("dialect".to_owned(), dialect)].into_iter().collect() + }); let current_path = std::env::current_dir().unwrap(); let ignore_file = ignore::IgnoreFile::new_from_root(¤t_path).unwrap(); @@ -90,13 +92,13 @@ where config, ignorer, collect_parse_errors, - dialect_override, + cli_overrides, ), Ok(true) => commands_lint::run_lint_stdin( config, args.format, collect_parse_errors, - dialect_override, + cli_overrides, ), }, Commands::Fix(args) => match is_std_in_flag_input(&args.paths) { @@ -109,13 +111,13 @@ where config, ignorer, collect_parse_errors, - dialect_override, + cli_overrides, ), Ok(true) => commands_fix::run_fix_stdin( config, args.format, collect_parse_errors, - dialect_override, + cli_overrides, ), }, Commands::Lsp => { @@ -147,7 +149,7 @@ pub(crate) fn linter( config: FluffConfig, format: Format, collect_parse_errors: bool, - dialect_override: Option, + cli_overrides: Option>, ) -> Linter { let formatter: Arc = match format { Format::Human => { @@ -170,15 +172,11 @@ pub(crate) fn linter( } }; - let mut config = config; - if let Some(dialect) = dialect_override { - config - .override_dialect(dialect) - .expect("invalid dialect override"); - } - let mut linter = Linter::new(config, Some(formatter), None, collect_parse_errors); - if let Some(dialect) = dialect_override { - linter.set_dialect_override(dialect); - } - linter + Linter::new( + config, + Some(formatter), + None, + collect_parse_errors, + cli_overrides, + ) } diff --git a/crates/cli/tests/ignore_data_directory.rs b/crates/cli/tests/ignore_data_directory.rs index b21ff6348..ffb22860b 100644 --- a/crates/cli/tests/ignore_data_directory.rs +++ b/crates/cli/tests/ignore_data_directory.rs @@ -215,6 +215,7 @@ fn test_lint_paths_traverses_ignored_directories() { None, None, false, + None, ); // Create a dummy ignorer that doesn't ignore anything (to test the current broken behavior) diff --git a/crates/lib-wasm/src/lib.rs b/crates/lib-wasm/src/lib.rs index 2c9d37151..84164d3f5 100644 --- a/crates/lib-wasm/src/lib.rs +++ b/crates/lib-wasm/src/lib.rs @@ -73,7 +73,7 @@ impl Linter { #[wasm_bindgen(constructor)] pub fn new(source: &str) -> Self { Self { - base: SqruffLinter::new(FluffConfig::from_source(source, None), None, None, true), + base: SqruffLinter::new(FluffConfig::from_source(source, None), None, None, true, None), } } diff --git a/crates/lib/benches/depth_map.rs b/crates/lib/benches/depth_map.rs index 2dd438c16..e11e89ece 100644 --- a/crates/lib/benches/depth_map.rs +++ b/crates/lib/benches/depth_map.rs @@ -71,7 +71,7 @@ SELECT construct_depth_info('uuid-2'); SELECT construct_depth_info('uuid-3');"#; fn depth_map(c: &mut Criterion) { - let linter = Linter::new(FluffConfig::default(), None, None, false); + let linter = Linter::new(FluffConfig::default(), None, None, false, None); let tables = Tables::default(); let tree = linter .parse_string(&tables, COMPLEX_QUERY, None) diff --git a/crates/lib/benches/fix.rs b/crates/lib/benches/fix.rs index 7b88826a1..ab27cb9c1 100644 --- a/crates/lib/benches/fix.rs +++ b/crates/lib/benches/fix.rs @@ -71,6 +71,7 @@ fn fix(c: &mut Criterion) { None, None, false, + None, ); for (name, source) in passes { let tables = Tables::default(); diff --git a/crates/lib/src/core/linter/core.rs b/crates/lib/src/core/linter/core.rs index a46e823a4..5fec92baa 100644 --- a/crates/lib/src/core/linter/core.rs +++ b/crates/lib/src/core/linter/core.rs @@ -19,7 +19,6 @@ use itertools::Itertools; use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; use smol_str::{SmolStr, ToSmolStr}; use sqruff_lib_core::dialects::Dialect; -use sqruff_lib_core::dialects::init::DialectKind; use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet}; use sqruff_lib_core::errors::{ SQLBaseError, SQLFluffUserError, SQLLexError, SQLLintError, SQLParseError, @@ -40,8 +39,8 @@ pub struct Linter { /// include_parse_errors is a flag to indicate whether to include parse errors in the output include_parse_errors: bool, - /// CLI dialect override, applied on top of any per-file config. - dialect_override: Option, + /// CLI overrides (e.g. --dialect), applied on top of any per-file config. + cli_overrides: Option>, } impl Linter { @@ -50,6 +49,7 @@ impl Linter { formatter: Option>, templater: Option<&'static dyn Templater>, include_parse_errors: bool, + cli_overrides: Option>, ) -> Linter { let templater: &'static dyn Templater = match templater { Some(templater) => templater, @@ -61,7 +61,7 @@ impl Linter { templater, rules: OnceLock::new(), include_parse_errors, - dialect_override: None, + cli_overrides, } } @@ -76,17 +76,8 @@ impl Linter { } } - pub fn set_dialect_override(&mut self, dialect: DialectKind) { - self.dialect_override = Some(dialect); - } - /// Update the linter's config, resetting cached templater and rules. - fn set_config(&mut self, mut config: FluffConfig) { - if let Some(dialect) = self.dialect_override { - config - .override_dialect(dialect) - .expect("invalid dialect override"); - } + fn set_config(&mut self, config: FluffConfig) { self.templater = Self::get_templater(&config); self.rules = OnceLock::new(); self.config = config; @@ -188,7 +179,11 @@ impl Linter { for (config_dir, group_paths) in groups { // Load config for this group. if let Some(dir) = &config_dir { - let config = FluffConfig::from_path(dir, None, false, None) + let overrides = self + .cli_overrides + .as_ref() + .map(|o| o.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); + let config = FluffConfig::from_path(dir, None, false, overrides) .unwrap_or_else(|_| FluffConfig::default()); self.set_config(config); } @@ -868,6 +863,7 @@ mod tests { None, None, false, + None, ); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path("test/fixtures/lexer".into(), None, None, None, None, None); @@ -887,6 +883,7 @@ mod tests { None, None, false, + None, ); // Assuming Linter has a new() method for initialization let paths = normalise_paths(lntr.paths_from_path( "test/fixtures/linter".into(), @@ -907,7 +904,7 @@ mod tests { // FluffConfig let config = FluffConfig::new(<_>::default(), None, None).with_sql_file_exts(vec![".txt".into()]); - let lntr = Linter::new(config, None, None, false); // Assuming Linter has a new() method for initialization + let lntr = Linter::new(config, None, None, false, None); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path("test/fixtures/linter".into(), None, None, None, None, None); @@ -930,6 +927,7 @@ mod tests { None, None, false, + None, ); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path( "test/fixtures/linter/indentation_errors.sql".into(), @@ -970,6 +968,7 @@ mod tests { None, None, false, + None, ); let tables = Tables::default(); let parsed = linter.parse_string(&tables, "", None).unwrap(); @@ -1002,6 +1001,7 @@ mod tests { None, None, false, + None, ); let tables = Tables::default(); let _parsed = linter.parse_string(&tables, &sql, None).unwrap(); @@ -1032,7 +1032,7 @@ mod tests { std::fs::write(ansi_dir.join("test.sql"), "SELECT a FROM t WHERE a >= 1\n").unwrap(); let config = FluffConfig::default(); - let mut linter = Linter::new(config, None, None, false); + let mut linter = Linter::new(config, None, None, false, None); let result = linter .lint_paths( diff --git a/crates/lib/src/core/rules/noqa.rs b/crates/lib/src/core/rules/noqa.rs index 14139365a..496414d6c 100644 --- a/crates/lib/src/core/rules/noqa.rs +++ b/crates/lib/src/core/rules/noqa.rs @@ -633,6 +633,7 @@ rules = AL02 None, None, false, + None, ); let sql = r#"SELECT @@ -666,6 +667,7 @@ rules = AL02 None, None, false, + None, ); let linter_with_disabled = Linter::new( FluffConfig::from_source( @@ -680,6 +682,7 @@ disable_noqa = True None, None, false, + None, ); let sql = r#"SELECT @@ -710,6 +713,7 @@ rules = AL02 None, None, false, + None, ); let sql_disable_rule = r#"SELECT col_a a, diff --git a/crates/lib/src/core/test_functions.rs b/crates/lib/src/core/test_functions.rs index e9d42944e..9ac074245 100644 --- a/crates/lib/src/core/test_functions.rs +++ b/crates/lib/src/core/test_functions.rs @@ -7,7 +7,7 @@ use crate::core::linter::core::Linter; pub fn parse_ansi_string(sql: &str) -> ErasedSegment { let tables = Tables::default(); - let linter = Linter::new(<_>::default(), None, None, false); + let linter = Linter::new(<_>::default(), None, None, false, None); linter .parse_string(&tables, sql, None) .unwrap() diff --git a/crates/lib/src/rules/layout/lt05.rs b/crates/lib/src/rules/layout/lt05.rs index 1e17dc882..3ad934b99 100644 --- a/crates/lib/src/rules/layout/lt05.rs +++ b/crates/lib/src/rules/layout/lt05.rs @@ -201,7 +201,7 @@ SELECT ) AS result FROM t "; - let linter = Linter::new(FluffConfig::default(), None, None, true); + let linter = Linter::new(FluffConfig::default(), None, None, true, None); let result = linter.lint_string(sql, None, true).unwrap(); let fixed = result.fix_string(); diff --git a/crates/lib/src/templaters/placeholder.rs b/crates/lib/src/templaters/placeholder.rs index bba1db808..ff6895a83 100644 --- a/crates/lib/src/templaters/placeholder.rs +++ b/crates/lib/src/templaters/placeholder.rs @@ -805,7 +805,7 @@ param_style = percent ); let sql = "SELECT a,b FROM users WHERE a = %s"; - let mut linter = Linter::new(config, None, None, false); + let mut linter = Linter::new(config, None, None, false, None); let result = linter.lint_string_wrapped(sql, true).unwrap().fix_string(); assert_eq!(result, "SELECT\n a,\n b\nFROM users\nWHERE a = %s\n"); diff --git a/crates/lib/src/tests.rs b/crates/lib/src/tests.rs index 541f9a7c1..60c6b6bb6 100644 --- a/crates/lib/src/tests.rs +++ b/crates/lib/src/tests.rs @@ -195,6 +195,7 @@ fn test_dialect_ansi_specific_segment_not_parse() { None, None, false, + None, ); let tables = Tables::default(); let parsed = lnt.parse_string(&tables, raw, None).unwrap(); @@ -216,6 +217,7 @@ fn test_dialect_ansi_is_whitespace() { None, None, false, + None, ); let file_content = std::fs::read_to_string( "../lib-dialects/test/fixtures/dialects/ansi/sqlfluff/select_in_multiline_comment.sql", @@ -249,6 +251,7 @@ fn test_dialect_ansi_parse_indented_joins() { None, None, false, + None, ); for (sql_string, meta_loc) in cases { diff --git a/crates/lib/src/utils/reflow/reindent.rs b/crates/lib/src/utils/reflow/reindent.rs index 855de900f..3eab7a7a4 100644 --- a/crates/lib/src/utils/reflow/reindent.rs +++ b/crates/lib/src/utils/reflow/reindent.rs @@ -1725,7 +1725,7 @@ mod tests { use crate::core::linter::core::Linter; let sql = "with a as (select 1\nfrom t join u v on\n1=1\n)\nselect * from a\n"; - let linter = Linter::new(<_>::default(), None, None, false); + let linter = Linter::new(<_>::default(), None, None, false, None); let result = linter.lint_string(sql, None, false).unwrap(); // The panic is caught by catch_unwind and surfaced as an // "Unexpected exception" violation. Assert none are present. diff --git a/crates/lib/tests/rules.rs b/crates/lib/tests/rules.rs index ffe47edef..2d2a86c1e 100644 --- a/crates/lib/tests/rules.rs +++ b/crates/lib/tests/rules.rs @@ -54,7 +54,7 @@ fn main() { let mut args = Args::default(); args.parse_args(std::env::args().skip(1)); - let mut linter = Linter::new(FluffConfig::default(), None, None, true); + let mut linter = Linter::new(FluffConfig::default(), None, None, true, None); let mut core = HashMap::new(); core.insert( "core".to_string(), @@ -151,7 +151,7 @@ fn main() { // Recreate linter with proper templater after all config is set up let templater = Linter::get_templater(linter.config()); - linter = Linter::new(linter.config().clone(), None, Some(templater), true); + linter = Linter::new(linter.config().clone(), None, Some(templater), true, None); } match case.kind { @@ -174,7 +174,7 @@ dialect = {dialect} ", None); - let mut linter = Linter::new(config, None, None, true); + let mut linter = Linter::new(config, None, None, true, None); let pass_str = r"{pass_str}"; @@ -215,7 +215,7 @@ dialect = {dialect} // Recreate linter with default templater to avoid leaking // the custom templater (e.g. placeholder) into subsequent tests. let templater = Linter::get_templater(linter.config()); - linter = Linter::new(linter.config().clone(), None, Some(templater), true); + linter = Linter::new(linter.config().clone(), None, Some(templater), true, None); } } } diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index eb567a0f0..41582da43 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -101,7 +101,7 @@ impl Wasm { impl LanguageServer { pub fn new(send_diagnostics_callback: impl Fn(PublishDiagnosticsParams) + 'static) -> Self { Self { - linter: Linter::new(load_config(), None, None, false), + linter: Linter::new(load_config(), None, None, false, None), send_diagnostics_callback: Box::new(send_diagnostics_callback), documents: HashMap::new(), } From 3a0ed26553d6bacf6127cb1e3dbf306e426cc87a Mon Sep 17 00:00:00 2001 From: Gary Miguel Date: Wed, 18 Feb 2026 20:38:35 +0000 Subject: [PATCH 3/3] refactor: move per-file config resolution to CLI layer Instead of storing CLI overrides on the Linter and doing per-file config resolution inside lint_paths, the grouping and config resolution now happens in the CLI layer (commands_lint/commands_fix). Each config group gets a fully-resolved FluffConfig with --dialect already applied, so the Linter only ever sees a single resolved config with no override mechanism needed. This also fixes: - --dialect being ignored for stdin and parse commands - --config being overridden by per-file config discovery - Files with no .sqruff in ancestry inheriting stale config from a previous group --- crates/cli-lib/src/commands_fix.rs | 20 ++-- crates/cli-lib/src/commands_lint.rs | 113 ++++++++++++++++++++-- crates/cli-lib/src/commands_parse.rs | 2 +- crates/cli-lib/src/lib.rs | 68 ++++++------- crates/cli/tests/ignore_data_directory.rs | 1 - crates/lib-wasm/src/lib.rs | 2 +- crates/lib/benches/depth_map.rs | 2 +- crates/lib/benches/fix.rs | 1 - crates/lib/src/core/linter/core.rs | 111 ++++++++------------- crates/lib/src/core/rules/noqa.rs | 4 - crates/lib/src/core/test_functions.rs | 2 +- crates/lib/src/rules/layout/lt05.rs | 2 +- crates/lib/src/templaters/placeholder.rs | 2 +- crates/lib/src/tests.rs | 3 - crates/lib/src/utils/reflow/reindent.rs | 2 +- crates/lib/tests/rules.rs | 8 +- crates/lsp/src/lib.rs | 2 +- 17 files changed, 204 insertions(+), 141 deletions(-) diff --git a/crates/cli-lib/src/commands_fix.rs b/crates/cli-lib/src/commands_fix.rs index fba4f1bae..da97a987e 100644 --- a/crates/cli-lib/src/commands_fix.rs +++ b/crates/cli-lib/src/commands_fix.rs @@ -2,7 +2,7 @@ use crate::commands::FixArgs; use crate::commands::Format; use crate::linter; use sqruff_lib::core::config::FluffConfig; -use std::collections::HashMap; +use sqruff_lib_core::dialects::init::DialectKind; use std::path::Path; pub(crate) fn run_fix( @@ -10,11 +10,20 @@ pub(crate) fn run_fix( config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, - cli_overrides: Option>, + dialect_override: Option, ) -> i32 { let FixArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors, cli_overrides); - let result = match linter.lint_paths(paths, true, &ignorer) { + let mut linter = linter(config.clone(), format, collect_parse_errors); + + let result = crate::commands_lint::lint_paths_with_per_file_config( + &mut linter, + paths, + true, + &ignorer, + &config, + dialect_override, + ); + let result = match result { Ok(result) => result, Err(e) => { eprintln!("{}", e.value); @@ -48,11 +57,10 @@ pub(crate) fn run_fix_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, - cli_overrides: Option>, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors, cli_overrides); + let linter = linter(config, format, collect_parse_errors); let result = match linter.lint_string(&read_in, None, true) { Ok(result) => result, Err(e) => { diff --git a/crates/cli-lib/src/commands_lint.rs b/crates/cli-lib/src/commands_lint.rs index 1efaef760..cbb9cec11 100644 --- a/crates/cli-lib/src/commands_lint.rs +++ b/crates/cli-lib/src/commands_lint.rs @@ -1,19 +1,50 @@ use crate::commands::{Format, LintArgs}; use crate::linter; -use sqruff_lib::core::config::FluffConfig; +use sqruff_lib::core::config::{ConfigLoader, FluffConfig}; +use sqruff_lib::core::linter::linting_result::LintingResult; +use sqruff_lib_core::dialects::init::DialectKind; use std::collections::HashMap; -use std::path::Path; +use std::path::{Path, PathBuf}; + +/// Build a FluffConfig for files whose nearest config directory is `config_dir`. +/// Falls back to `base_config` when `config_dir` is None. +/// Applies `dialect_override` on top if provided. +fn config_for_group( + config_dir: Option<&Path>, + base_config: &FluffConfig, + dialect_override: Option, +) -> FluffConfig { + let mut config = match config_dir { + Some(dir) => FluffConfig::from_path(dir, None, false, None) + .unwrap_or_else(|_| base_config.clone()), + None => base_config.clone(), + }; + if let Some(dialect) = dialect_override { + // Unwrap is safe: dialect was already validated in the CLI. + config.override_dialect(dialect).unwrap(); + } + config +} pub(crate) fn run_lint( args: LintArgs, config: FluffConfig, ignorer: impl Fn(&Path) -> bool + Send + Sync, collect_parse_errors: bool, - cli_overrides: Option>, + dialect_override: Option, ) -> i32 { let LintArgs { paths, format } = args; - let mut linter = linter(config, format, collect_parse_errors, cli_overrides); - let result = match linter.lint_paths(paths, false, &ignorer) { + let mut linter = linter(config.clone(), format, collect_parse_errors); + + let result = lint_paths_with_per_file_config( + &mut linter, + paths, + false, + &ignorer, + &config, + dialect_override, + ); + let result = match result { Ok(result) => result, Err(e) => { eprintln!("{}", e.value); @@ -30,11 +61,10 @@ pub(crate) fn run_lint_stdin( config: FluffConfig, format: Format, collect_parse_errors: bool, - cli_overrides: Option>, ) -> i32 { let read_in = crate::stdin::read_std_in().unwrap(); - let linter = linter(config, format, collect_parse_errors, cli_overrides); + let linter = linter(config, format, collect_parse_errors); let result = match linter.lint_string(&read_in, None, false) { Ok(result) => result, Err(e) => { @@ -47,3 +77,72 @@ pub(crate) fn run_lint_stdin( result.has_violations() as i32 } + +/// Expand paths, group files by nearest config directory, and lint each group +/// with the appropriate config. When `dialect_override` is None (e.g. because +/// --config was given explicitly), per-file config resolution is skipped. +pub(crate) fn lint_paths_with_per_file_config( + linter: &mut sqruff_lib::core::linter::core::Linter, + paths: Vec, + fix: bool, + ignorer: &(dyn Fn(&Path) -> bool + Send + Sync), + base_config: &FluffConfig, + dialect_override: Option, +) -> Result { + // Expand directories to individual files. + let mut expanded: Vec = Vec::new(); + let input_paths = if paths.is_empty() { + vec![std::env::current_dir().unwrap()] + } else { + paths + }; + for path in input_paths { + if path.is_file() { + expanded.push(path); + } else { + for p in linter.paths_from_path(path, None, None, None, None, Some(ignorer)) { + expanded.push(PathBuf::from(p)); + } + } + } + + let expanded: Vec = expanded + .into_iter() + .filter(|path| { + let should_ignore = ignorer(path); + if should_ignore { + log::debug!( + "Filtering out ignored file '{}' from final processing list", + path.display() + ); + } + !should_ignore + }) + .collect(); + + if expanded.is_empty() { + return Ok(LintingResult::new(Vec::new())); + } + + if dialect_override.is_some() { + // Group files by nearest config directory. + let mut groups: HashMap, Vec> = HashMap::new(); + for path in expanded { + let config_dir = ConfigLoader::find_nearest_config_dir(&path); + groups.entry(config_dir).or_default().push(path); + } + + let mut all_files = Vec::new(); + for (config_dir, group_paths) in groups { + let config = + config_for_group(config_dir.as_deref(), base_config, dialect_override); + linter.set_config(config); + let result = linter.lint_paths(group_paths, fix, ignorer)?; + all_files.extend(result); + } + Ok(LintingResult::new(all_files)) + } else { + // No per-file resolution (explicit --config or no --dialect). + linter.lint_paths(expanded, fix, ignorer) + } +} diff --git a/crates/cli-lib/src/commands_parse.rs b/crates/cli-lib/src/commands_parse.rs index 9474252ba..e1a264c74 100644 --- a/crates/cli-lib/src/commands_parse.rs +++ b/crates/cli-lib/src/commands_parse.rs @@ -66,7 +66,7 @@ fn parse_and_output_tree( format: ParseFormat, ) -> i32 { // Create a linter and parse the SQL - let linter = Linter::new(config.clone(), None, None, true, None); + let linter = Linter::new(config.clone(), None, None, true); let tables = Tables::default(); match linter.parse_string(&tables, sql, Some(filename.to_string())) { diff --git a/crates/cli-lib/src/lib.rs b/crates/cli-lib/src/lib.rs index 09b2bb70d..6cdfacd69 100644 --- a/crates/cli-lib/src/lib.rs +++ b/crates/cli-lib/src/lib.rs @@ -46,7 +46,15 @@ where let cli = Cli::parse_from(args); let collect_parse_errors = cli.parsing_errors; - let config: FluffConfig = if let Some(config) = cli.config.as_ref() { + let dialect_override: Option = cli.dialect.map(|dialect| { + DialectKind::try_from(dialect.as_str()).unwrap_or_else(|e| { + eprintln!("{}", e); + std::process::exit(1); + }) + }); + + let explicit_config = cli.config.is_some(); + let mut config: FluffConfig = if let Some(config) = cli.config.as_ref() { if !Path::new(config).is_file() { eprintln!( "The specified config file '{}' does not exist.", @@ -57,21 +65,15 @@ where }; FluffConfig::from_file(Path::new(config)) } else { - // Load a base config from cwd ancestors. Per-file config resolution - // happens inside the Linter during lint_paths. FluffConfig::from_root(None, false, None).unwrap() }; - // Build CLI overrides (e.g. --dialect) to apply on top of per-file configs. - let cli_overrides: Option> = - cli.dialect.map(|dialect| { - // Validate the dialect name early. - DialectKind::try_from(dialect.as_str()).unwrap_or_else(|e| { - eprintln!("{}", e); - std::process::exit(1); - }); - [("dialect".to_owned(), dialect)].into_iter().collect() + if let Some(dialect) = dialect_override { + config.override_dialect(dialect).unwrap_or_else(|e| { + eprintln!("{}", e); + std::process::exit(1); }); + } let current_path = std::env::current_dir().unwrap(); let ignore_file = ignore::IgnoreFile::new_from_root(¤t_path).unwrap(); @@ -81,6 +83,15 @@ where move |path: &Path| ignore_file.is_ignored(path) }; + // Per-file config resolution is only used when no explicit --config was + // given. When it is used, dialect_override is re-applied on top of each + // per-file config. + let per_file_dialect = if explicit_config { + None + } else { + dialect_override + }; + match cli.command { Commands::Lint(args) => match is_std_in_flag_input(&args.paths) { Err(e) => { @@ -92,14 +103,9 @@ where config, ignorer, collect_parse_errors, - cli_overrides, - ), - Ok(true) => commands_lint::run_lint_stdin( - config, - args.format, - collect_parse_errors, - cli_overrides, + per_file_dialect, ), + Ok(true) => commands_lint::run_lint_stdin(config, args.format, collect_parse_errors), }, Commands::Fix(args) => match is_std_in_flag_input(&args.paths) { Err(e) => { @@ -111,14 +117,9 @@ where config, ignorer, collect_parse_errors, - cli_overrides, - ), - Ok(true) => commands_fix::run_fix_stdin( - config, - args.format, - collect_parse_errors, - cli_overrides, + per_file_dialect, ), + Ok(true) => commands_fix::run_fix_stdin(config, args.format, collect_parse_errors), }, Commands::Lsp => { sqruff_lsp::run(); @@ -145,12 +146,7 @@ where } } -pub(crate) fn linter( - config: FluffConfig, - format: Format, - collect_parse_errors: bool, - cli_overrides: Option>, -) -> Linter { +pub(crate) fn linter(config: FluffConfig, format: Format, collect_parse_errors: bool) -> Linter { let formatter: Arc = match format { Format::Human => { let output_stream = std::io::stderr().into(); @@ -172,11 +168,5 @@ pub(crate) fn linter( } }; - Linter::new( - config, - Some(formatter), - None, - collect_parse_errors, - cli_overrides, - ) + Linter::new(config, Some(formatter), None, collect_parse_errors) } diff --git a/crates/cli/tests/ignore_data_directory.rs b/crates/cli/tests/ignore_data_directory.rs index ffb22860b..b21ff6348 100644 --- a/crates/cli/tests/ignore_data_directory.rs +++ b/crates/cli/tests/ignore_data_directory.rs @@ -215,7 +215,6 @@ fn test_lint_paths_traverses_ignored_directories() { None, None, false, - None, ); // Create a dummy ignorer that doesn't ignore anything (to test the current broken behavior) diff --git a/crates/lib-wasm/src/lib.rs b/crates/lib-wasm/src/lib.rs index 84164d3f5..2c9d37151 100644 --- a/crates/lib-wasm/src/lib.rs +++ b/crates/lib-wasm/src/lib.rs @@ -73,7 +73,7 @@ impl Linter { #[wasm_bindgen(constructor)] pub fn new(source: &str) -> Self { Self { - base: SqruffLinter::new(FluffConfig::from_source(source, None), None, None, true, None), + base: SqruffLinter::new(FluffConfig::from_source(source, None), None, None, true), } } diff --git a/crates/lib/benches/depth_map.rs b/crates/lib/benches/depth_map.rs index e11e89ece..2dd438c16 100644 --- a/crates/lib/benches/depth_map.rs +++ b/crates/lib/benches/depth_map.rs @@ -71,7 +71,7 @@ SELECT construct_depth_info('uuid-2'); SELECT construct_depth_info('uuid-3');"#; fn depth_map(c: &mut Criterion) { - let linter = Linter::new(FluffConfig::default(), None, None, false, None); + let linter = Linter::new(FluffConfig::default(), None, None, false); let tables = Tables::default(); let tree = linter .parse_string(&tables, COMPLEX_QUERY, None) diff --git a/crates/lib/benches/fix.rs b/crates/lib/benches/fix.rs index ab27cb9c1..7b88826a1 100644 --- a/crates/lib/benches/fix.rs +++ b/crates/lib/benches/fix.rs @@ -71,7 +71,6 @@ fn fix(c: &mut Criterion) { None, None, false, - None, ); for (name, source) in passes { let tables = Tables::default(); diff --git a/crates/lib/src/core/linter/core.rs b/crates/lib/src/core/linter/core.rs index 5fec92baa..63b7cd903 100644 --- a/crates/lib/src/core/linter/core.rs +++ b/crates/lib/src/core/linter/core.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use crate::Formatter; -use crate::core::config::{ConfigLoader, FluffConfig}; +use crate::core::config::FluffConfig; use crate::core::linter::common::{ParsedString, RenderedFile}; use crate::core::linter::linted_file::LintedFile; use crate::core::linter::linting_result::LintingResult; @@ -38,9 +38,6 @@ pub struct Linter { /// include_parse_errors is a flag to indicate whether to include parse errors in the output include_parse_errors: bool, - - /// CLI overrides (e.g. --dialect), applied on top of any per-file config. - cli_overrides: Option>, } impl Linter { @@ -49,7 +46,6 @@ impl Linter { formatter: Option>, templater: Option<&'static dyn Templater>, include_parse_errors: bool, - cli_overrides: Option>, ) -> Linter { let templater: &'static dyn Templater = match templater { Some(templater) => templater, @@ -61,7 +57,6 @@ impl Linter { templater, rules: OnceLock::new(), include_parse_errors, - cli_overrides, } } @@ -77,7 +72,7 @@ impl Linter { } /// Update the linter's config, resetting cached templater and rules. - fn set_config(&mut self, config: FluffConfig) { + pub fn set_config(&mut self, config: FluffConfig) { self.templater = Self::get_templater(&config); self.rules = OnceLock::new(); self.config = config; @@ -166,52 +161,31 @@ impl Linter { }) .collect_vec(); - // Group files by their nearest config directory so each group uses the - // right .sqruff/.sqlfluff config. - let mut groups: HashMap, Vec> = HashMap::new(); - for path in &paths { - let config_dir = ConfigLoader::find_nearest_config_dir(Path::new(path)); - groups.entry(config_dir).or_default().push(path.clone()); - } - let mut files = Vec::with_capacity(paths.len()); - for (config_dir, group_paths) in groups { - // Load config for this group. - if let Some(dir) = &config_dir { - let overrides = self - .cli_overrides - .as_ref() - .map(|o| o.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); - let config = FluffConfig::from_path(dir, None, false, overrides) - .unwrap_or_else(|_| FluffConfig::default()); - self.set_config(config); - } - - match self.templater.processing_mode() { - ProcessingMode::Parallel => { - let results: Vec<_> = group_paths - .par_iter() - .map(|path| { - let rendered = self.render_file(path.clone()); - self.lint_rendered(rendered, fix) - }) - .collect(); - for result in results { - files.push(result?); - } + match self.templater.processing_mode() { + ProcessingMode::Parallel => { + let results: Vec<_> = paths + .par_iter() + .map(|path| { + let rendered = self.render_file(path.clone()); + self.lint_rendered(rendered, fix) + }) + .collect(); + for result in results { + files.push(result?); } - ProcessingMode::Batch => { - let rendered_files = self.render_files_batch(&group_paths); - for rendered in rendered_files { - files.push(self.lint_rendered(rendered, fix)?); - } + } + ProcessingMode::Batch => { + let rendered_files = self.render_files_batch(&paths); + for rendered in rendered_files { + files.push(self.lint_rendered(rendered, fix)?); } - ProcessingMode::Sequential => { - for path in &group_paths { - let rendered = self.render_file(path.clone()); - files.push(self.lint_rendered(rendered, fix)?); - } + } + ProcessingMode::Sequential => { + for path in &paths { + let rendered = self.render_file(path.clone()); + files.push(self.lint_rendered(rendered, fix)?); } } } @@ -654,7 +628,7 @@ impl Linter { // up to the current directory. // If the current directory is not a parent of the file we only // look for an ignore file in the direct parent of the file. - fn paths_from_path( + pub fn paths_from_path( &self, path: PathBuf, ignore_file_name: Option, @@ -863,7 +837,6 @@ mod tests { None, None, false, - None, ); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path("test/fixtures/lexer".into(), None, None, None, None, None); @@ -883,7 +856,6 @@ mod tests { None, None, false, - None, ); // Assuming Linter has a new() method for initialization let paths = normalise_paths(lntr.paths_from_path( "test/fixtures/linter".into(), @@ -904,7 +876,7 @@ mod tests { // FluffConfig let config = FluffConfig::new(<_>::default(), None, None).with_sql_file_exts(vec![".txt".into()]); - let lntr = Linter::new(config, None, None, false, None); // Assuming Linter has a new() method for initialization + let lntr = Linter::new(config, None, None, false); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path("test/fixtures/linter".into(), None, None, None, None, None); @@ -927,7 +899,6 @@ mod tests { None, None, false, - None, ); // Assuming Linter has a new() method for initialization let paths = lntr.paths_from_path( "test/fixtures/linter/indentation_errors.sql".into(), @@ -968,7 +939,6 @@ mod tests { None, None, false, - None, ); let tables = Tables::default(); let parsed = linter.parse_string(&tables, "", None).unwrap(); @@ -1001,7 +971,6 @@ mod tests { None, None, false, - None, ); let tables = Tables::default(); let _parsed = linter.parse_string(&tables, &sql, None).unwrap(); @@ -1031,21 +1000,27 @@ mod tests { std::fs::create_dir_all(&ansi_dir).unwrap(); std::fs::write(ansi_dir.join("test.sql"), "SELECT a FROM t WHERE a >= 1\n").unwrap(); - let config = FluffConfig::default(); - let mut linter = Linter::new(config, None, None, false, None); + let mut linter = Linter::new(FluffConfig::default(), None, None, false); - let result = linter - .lint_paths( - vec![bq_dir.join("test.sql"), ansi_dir.join("test.sql")], - false, - &|_| false, - ) + // Lint the bigquery file with bigquery config. + let bq_config = FluffConfig::from_path(base.join("bq").as_path(), None, false, None) .unwrap(); + assert_eq!( + bq_config.get_dialect().name, + sqruff_lib_core::dialects::init::DialectKind::Bigquery + ); + linter.set_config(bq_config); + let bq_result = linter + .lint_paths(vec![bq_dir.join("test.sql")], false, &|_| false) + .unwrap(); + assert_eq!(bq_result.len(), 1); - // Both files should lint without panicking (the bigquery file would - // fail to parse under the ansi dialect due to backtick-quoted project - // identifiers). - assert_eq!(result.len(), 2); + // Lint the ansi file with default config. + linter.set_config(FluffConfig::default()); + let ansi_result = linter + .lint_paths(vec![ansi_dir.join("test.sql")], false, &|_| false) + .unwrap(); + assert_eq!(ansi_result.len(), 1); let _ = std::fs::remove_dir_all(&base); } diff --git a/crates/lib/src/core/rules/noqa.rs b/crates/lib/src/core/rules/noqa.rs index 496414d6c..14139365a 100644 --- a/crates/lib/src/core/rules/noqa.rs +++ b/crates/lib/src/core/rules/noqa.rs @@ -633,7 +633,6 @@ rules = AL02 None, None, false, - None, ); let sql = r#"SELECT @@ -667,7 +666,6 @@ rules = AL02 None, None, false, - None, ); let linter_with_disabled = Linter::new( FluffConfig::from_source( @@ -682,7 +680,6 @@ disable_noqa = True None, None, false, - None, ); let sql = r#"SELECT @@ -713,7 +710,6 @@ rules = AL02 None, None, false, - None, ); let sql_disable_rule = r#"SELECT col_a a, diff --git a/crates/lib/src/core/test_functions.rs b/crates/lib/src/core/test_functions.rs index 9ac074245..e9d42944e 100644 --- a/crates/lib/src/core/test_functions.rs +++ b/crates/lib/src/core/test_functions.rs @@ -7,7 +7,7 @@ use crate::core::linter::core::Linter; pub fn parse_ansi_string(sql: &str) -> ErasedSegment { let tables = Tables::default(); - let linter = Linter::new(<_>::default(), None, None, false, None); + let linter = Linter::new(<_>::default(), None, None, false); linter .parse_string(&tables, sql, None) .unwrap() diff --git a/crates/lib/src/rules/layout/lt05.rs b/crates/lib/src/rules/layout/lt05.rs index 3ad934b99..1e17dc882 100644 --- a/crates/lib/src/rules/layout/lt05.rs +++ b/crates/lib/src/rules/layout/lt05.rs @@ -201,7 +201,7 @@ SELECT ) AS result FROM t "; - let linter = Linter::new(FluffConfig::default(), None, None, true, None); + let linter = Linter::new(FluffConfig::default(), None, None, true); let result = linter.lint_string(sql, None, true).unwrap(); let fixed = result.fix_string(); diff --git a/crates/lib/src/templaters/placeholder.rs b/crates/lib/src/templaters/placeholder.rs index ff6895a83..bba1db808 100644 --- a/crates/lib/src/templaters/placeholder.rs +++ b/crates/lib/src/templaters/placeholder.rs @@ -805,7 +805,7 @@ param_style = percent ); let sql = "SELECT a,b FROM users WHERE a = %s"; - let mut linter = Linter::new(config, None, None, false, None); + let mut linter = Linter::new(config, None, None, false); let result = linter.lint_string_wrapped(sql, true).unwrap().fix_string(); assert_eq!(result, "SELECT\n a,\n b\nFROM users\nWHERE a = %s\n"); diff --git a/crates/lib/src/tests.rs b/crates/lib/src/tests.rs index 60c6b6bb6..541f9a7c1 100644 --- a/crates/lib/src/tests.rs +++ b/crates/lib/src/tests.rs @@ -195,7 +195,6 @@ fn test_dialect_ansi_specific_segment_not_parse() { None, None, false, - None, ); let tables = Tables::default(); let parsed = lnt.parse_string(&tables, raw, None).unwrap(); @@ -217,7 +216,6 @@ fn test_dialect_ansi_is_whitespace() { None, None, false, - None, ); let file_content = std::fs::read_to_string( "../lib-dialects/test/fixtures/dialects/ansi/sqlfluff/select_in_multiline_comment.sql", @@ -251,7 +249,6 @@ fn test_dialect_ansi_parse_indented_joins() { None, None, false, - None, ); for (sql_string, meta_loc) in cases { diff --git a/crates/lib/src/utils/reflow/reindent.rs b/crates/lib/src/utils/reflow/reindent.rs index 3eab7a7a4..855de900f 100644 --- a/crates/lib/src/utils/reflow/reindent.rs +++ b/crates/lib/src/utils/reflow/reindent.rs @@ -1725,7 +1725,7 @@ mod tests { use crate::core::linter::core::Linter; let sql = "with a as (select 1\nfrom t join u v on\n1=1\n)\nselect * from a\n"; - let linter = Linter::new(<_>::default(), None, None, false, None); + let linter = Linter::new(<_>::default(), None, None, false); let result = linter.lint_string(sql, None, false).unwrap(); // The panic is caught by catch_unwind and surfaced as an // "Unexpected exception" violation. Assert none are present. diff --git a/crates/lib/tests/rules.rs b/crates/lib/tests/rules.rs index 2d2a86c1e..ffe47edef 100644 --- a/crates/lib/tests/rules.rs +++ b/crates/lib/tests/rules.rs @@ -54,7 +54,7 @@ fn main() { let mut args = Args::default(); args.parse_args(std::env::args().skip(1)); - let mut linter = Linter::new(FluffConfig::default(), None, None, true, None); + let mut linter = Linter::new(FluffConfig::default(), None, None, true); let mut core = HashMap::new(); core.insert( "core".to_string(), @@ -151,7 +151,7 @@ fn main() { // Recreate linter with proper templater after all config is set up let templater = Linter::get_templater(linter.config()); - linter = Linter::new(linter.config().clone(), None, Some(templater), true, None); + linter = Linter::new(linter.config().clone(), None, Some(templater), true); } match case.kind { @@ -174,7 +174,7 @@ dialect = {dialect} ", None); - let mut linter = Linter::new(config, None, None, true, None); + let mut linter = Linter::new(config, None, None, true); let pass_str = r"{pass_str}"; @@ -215,7 +215,7 @@ dialect = {dialect} // Recreate linter with default templater to avoid leaking // the custom templater (e.g. placeholder) into subsequent tests. let templater = Linter::get_templater(linter.config()); - linter = Linter::new(linter.config().clone(), None, Some(templater), true, None); + linter = Linter::new(linter.config().clone(), None, Some(templater), true); } } } diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 41582da43..eb567a0f0 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -101,7 +101,7 @@ impl Wasm { impl LanguageServer { pub fn new(send_diagnostics_callback: impl Fn(PublishDiagnosticsParams) + 'static) -> Self { Self { - linter: Linter::new(load_config(), None, None, false, None), + linter: Linter::new(load_config(), None, None, false), send_diagnostics_callback: Box::new(send_diagnostics_callback), documents: HashMap::new(), }