diff --git a/crates/ruff/src/commands/show_settings.rs b/crates/ruff/src/commands/show_settings.rs index ec4b0d5482..8c38285955 100644 --- a/crates/ruff/src/commands/show_settings.rs +++ b/crates/ruff/src/commands/show_settings.rs @@ -29,10 +29,10 @@ pub(crate) fn show_settings( bail!("No files found under the given path"); }; - let settings = resolver.resolve(&path); + let (settings, config_path) = resolver.resolve_with_path(&path); writeln!(writer, "Resolved settings for: \"{}\"", path.display())?; - if let Some(settings_path) = pyproject_config.path.as_ref() { + if let Some(settings_path) = config_path { writeln!(writer, "Settings path: \"{}\"", settings_path.display())?; } write!(writer, "{settings}")?; diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 0e920cc3bc..9d7ae54560 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -16,6 +16,7 @@ success: true exit_code: 0 ----- stdout ----- Resolved settings for: "[TMP]/foo/test.py" +Settings path: "[TMP]/foo/pyproject.toml" # General Settings cache_dir = "[TMP]/foo/.ruff_cache" diff --git a/crates/ruff/tests/show_settings.rs b/crates/ruff/tests/show_settings.rs index 9a20b16afd..33db97b75e 100644 --- a/crates/ruff/tests/show_settings.rs +++ b/crates/ruff/tests/show_settings.rs @@ -50,6 +50,56 @@ ignore = [ Ok(()) } +#[test] +fn display_settings_from_nested_directory() -> anyhow::Result<()> { + let tempdir = TempDir::new().context("Failed to create temp directory.")?; + + // Tempdir path's on macos are symlinks, which doesn't play nicely with + // our snapshot filtering. + let project_dir = + dunce::canonicalize(tempdir.path()).context("Failed to canonical tempdir path.")?; + + // Root pyproject.toml. + std::fs::write( + project_dir.join("pyproject.toml"), + r#" +[tool.ruff] +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F"] +"#, + )?; + + // Create a subdirectory with its own pyproject.toml. + let subdir = project_dir.join("subdir"); + std::fs::create_dir(&subdir)?; + + std::fs::write( + subdir.join("pyproject.toml"), + r#" +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I"] +"#, + )?; + + std::fs::write(subdir.join("test.py"), r#"import os"#).context("Failed to write test.py.")?; + + insta::with_settings!({filters => vec![ + (&*tempdir_filter(&project_dir), "/"), + (r#"\\(\w\w|\s|\.|")"#, "/$1"), + ]}, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--show-settings", "subdir/test.py"]) + .current_dir(&project_dir)); + }); + + Ok(()) +} + fn tempdir_filter(project_dir: &Path) -> String { format!(r#"{}\\?/?"#, regex::escape(project_dir.to_str().unwrap())) } diff --git a/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap new file mode 100644 index 0000000000..1141adb331 --- /dev/null +++ b/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap @@ -0,0 +1,410 @@ +--- +source: crates/ruff/tests/show_settings.rs +info: + program: ruff + args: + - check + - "--show-settings" + - subdir/test.py +--- +success: true +exit_code: 0 +----- stdout ----- +Resolved settings for: "/subdir/test.py" +Settings path: "/subdir/pyproject.toml" + +# General Settings +cache_dir = "/subdir/.ruff_cache" +fix = false +fix_only = false +output_format = full +show_fixes = false +unsafe_fixes = hint + +# File Resolver Settings +file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", +] +file_resolver.extend_exclude = [] +file_resolver.force_exclude = false +file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", +] +file_resolver.extend_include = [] +file_resolver.respect_gitignore = true +file_resolver.project_root = "/subdir" + +# Linter Settings +linter.exclude = [] +linter.project_root = "/subdir" +linter.rules.enabled = [ + unsorted-imports (I001), + missing-required-import (I002), + mixed-spaces-and-tabs (E101), + multiple-imports-on-one-line (E401), + module-import-not-at-top-of-file (E402), + line-too-long (E501), + multiple-statements-on-one-line-colon (E701), + multiple-statements-on-one-line-semicolon (E702), + useless-semicolon (E703), + none-comparison (E711), + true-false-comparison (E712), + not-in-test (E713), + not-is-test (E714), + type-comparison (E721), + bare-except (E722), + lambda-assignment (E731), + ambiguous-variable-name (E741), + ambiguous-class-name (E742), + ambiguous-function-name (E743), + io-error (E902), + unused-import (F401), + import-shadowed-by-loop-var (F402), + undefined-local-with-import-star (F403), + late-future-import (F404), + undefined-local-with-import-star-usage (F405), + undefined-local-with-nested-import-star-usage (F406), + future-feature-not-defined (F407), + percent-format-invalid-format (F501), + percent-format-expected-mapping (F502), + percent-format-expected-sequence (F503), + percent-format-extra-named-arguments (F504), + percent-format-missing-argument (F505), + percent-format-mixed-positional-and-named (F506), + percent-format-positional-count-mismatch (F507), + percent-format-star-requires-sequence (F508), + percent-format-unsupported-format-character (F509), + string-dot-format-invalid-format (F521), + string-dot-format-extra-named-arguments (F522), + string-dot-format-extra-positional-arguments (F523), + string-dot-format-missing-arguments (F524), + string-dot-format-mixing-automatic (F525), + f-string-missing-placeholders (F541), + multi-value-repeated-key-literal (F601), + multi-value-repeated-key-variable (F602), + expressions-in-star-assignment (F621), + multiple-starred-expressions (F622), + assert-tuple (F631), + is-literal (F632), + invalid-print-syntax (F633), + if-tuple (F634), + break-outside-loop (F701), + continue-outside-loop (F702), + yield-outside-function (F704), + return-outside-function (F706), + default-except-not-last (F707), + forward-annotation-syntax-error (F722), + redefined-while-unused (F811), + undefined-name (F821), + undefined-export (F822), + undefined-local (F823), + unused-variable (F841), + unused-annotation (F842), + raise-not-implemented (F901), +] +linter.rules.should_fix = [ + unsorted-imports (I001), + missing-required-import (I002), + mixed-spaces-and-tabs (E101), + multiple-imports-on-one-line (E401), + module-import-not-at-top-of-file (E402), + line-too-long (E501), + multiple-statements-on-one-line-colon (E701), + multiple-statements-on-one-line-semicolon (E702), + useless-semicolon (E703), + none-comparison (E711), + true-false-comparison (E712), + not-in-test (E713), + not-is-test (E714), + type-comparison (E721), + bare-except (E722), + lambda-assignment (E731), + ambiguous-variable-name (E741), + ambiguous-class-name (E742), + ambiguous-function-name (E743), + io-error (E902), + unused-import (F401), + import-shadowed-by-loop-var (F402), + undefined-local-with-import-star (F403), + late-future-import (F404), + undefined-local-with-import-star-usage (F405), + undefined-local-with-nested-import-star-usage (F406), + future-feature-not-defined (F407), + percent-format-invalid-format (F501), + percent-format-expected-mapping (F502), + percent-format-expected-sequence (F503), + percent-format-extra-named-arguments (F504), + percent-format-missing-argument (F505), + percent-format-mixed-positional-and-named (F506), + percent-format-positional-count-mismatch (F507), + percent-format-star-requires-sequence (F508), + percent-format-unsupported-format-character (F509), + string-dot-format-invalid-format (F521), + string-dot-format-extra-named-arguments (F522), + string-dot-format-extra-positional-arguments (F523), + string-dot-format-missing-arguments (F524), + string-dot-format-mixing-automatic (F525), + f-string-missing-placeholders (F541), + multi-value-repeated-key-literal (F601), + multi-value-repeated-key-variable (F602), + expressions-in-star-assignment (F621), + multiple-starred-expressions (F622), + assert-tuple (F631), + is-literal (F632), + invalid-print-syntax (F633), + if-tuple (F634), + break-outside-loop (F701), + continue-outside-loop (F702), + yield-outside-function (F704), + return-outside-function (F706), + default-except-not-last (F707), + forward-annotation-syntax-error (F722), + redefined-while-unused (F811), + undefined-name (F821), + undefined-export (F822), + undefined-local (F823), + unused-variable (F841), + unused-annotation (F842), + raise-not-implemented (F901), +] +linter.per_file_ignores = {} +linter.safety_table.forced_safe = [] +linter.safety_table.forced_unsafe = [] +linter.unresolved_target_version = none +linter.per_file_target_version = {} +linter.preview = disabled +linter.explicit_preview_rules = false +linter.extension = ExtensionMapping({}) +linter.allowed_confusables = [] +linter.builtins = [] +linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ +linter.external = [] +linter.ignore_init_module_imports = true +linter.logger_objects = [] +linter.namespace_packages = [] +linter.src = [ + "/subdir", + "/subdir/src", +] +linter.tab_size = 4 +linter.line_length = 120 +linter.task_tags = [ + TODO, + FIXME, + XXX, +] +linter.typing_modules = [] +linter.typing_extensions = true + +# Linter Plugins +linter.flake8_annotations.mypy_init_return = false +linter.flake8_annotations.suppress_dummy_args = false +linter.flake8_annotations.suppress_none_returning = false +linter.flake8_annotations.allow_star_arg_any = false +linter.flake8_annotations.ignore_fully_untyped = false +linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, +] +linter.flake8_bandit.check_typed_exception = false +linter.flake8_bandit.extend_markup_names = [] +linter.flake8_bandit.allowed_markup_calls = [] +linter.flake8_bugbear.extend_immutable_calls = [] +linter.flake8_builtins.allowed_modules = [] +linter.flake8_builtins.ignorelist = [] +linter.flake8_builtins.strict_checking = false +linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false +linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* +linter.flake8_copyright.author = none +linter.flake8_copyright.min_file_size = 0 +linter.flake8_errmsg.max_string_length = 0 +linter.flake8_gettext.function_names = [ + _, + gettext, + ngettext, +] +linter.flake8_implicit_str_concat.allow_multiline = true +linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, +} +linter.flake8_import_conventions.banned_aliases = {} +linter.flake8_import_conventions.banned_from = [] +linter.flake8_pytest_style.fixture_parentheses = false +linter.flake8_pytest_style.parametrize_names_type = tuple +linter.flake8_pytest_style.parametrize_values_type = list +linter.flake8_pytest_style.parametrize_values_row_type = tuple +linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, +] +linter.flake8_pytest_style.raises_extend_require_match_for = [] +linter.flake8_pytest_style.mark_parentheses = false +linter.flake8_quotes.inline_quotes = double +linter.flake8_quotes.multiline_quotes = double +linter.flake8_quotes.docstring_quotes = double +linter.flake8_quotes.avoid_escape = true +linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, +] +linter.flake8_tidy_imports.ban_relative_imports = "parents" +linter.flake8_tidy_imports.banned_api = {} +linter.flake8_tidy_imports.banned_module_level_imports = [] +linter.flake8_type_checking.strict = false +linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, +] +linter.flake8_type_checking.runtime_required_base_classes = [] +linter.flake8_type_checking.runtime_required_decorators = [] +linter.flake8_type_checking.quote_annotations = false +linter.flake8_unused_arguments.ignore_variadic_names = false +linter.isort.required_imports = [] +linter.isort.combine_as_imports = false +linter.isort.force_single_line = false +linter.isort.force_sort_within_sections = false +linter.isort.detect_same_package = true +linter.isort.case_sensitive = false +linter.isort.force_wrap_aliases = false +linter.isort.force_to_top = [] +linter.isort.known_modules = {} +linter.isort.order_by_type = true +linter.isort.relative_imports_order = furthest_to_closest +linter.isort.single_line_exclusions = [] +linter.isort.split_on_trailing_comma = true +linter.isort.classes = [] +linter.isort.constants = [] +linter.isort.variables = [] +linter.isort.no_lines_before = [] +linter.isort.lines_after_imports = -1 +linter.isort.lines_between_types = 0 +linter.isort.forced_separate = [] +linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, +] +linter.isort.default_section = known { type = third_party } +linter.isort.no_sections = false +linter.isort.from_first = false +linter.isort.length_sort = false +linter.isort.length_sort_straight = false +linter.mccabe.max_complexity = 10 +linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, +] +linter.pep8_naming.classmethod_decorators = [] +linter.pep8_naming.staticmethod_decorators = [] +linter.pycodestyle.max_line_length = 120 +linter.pycodestyle.max_doc_length = none +linter.pycodestyle.ignore_overlong_task_comments = false +linter.pyflakes.extend_generics = [] +linter.pyflakes.allowed_unused_imports = [] +linter.pylint.allow_magic_value_types = [ + str, + bytes, +] +linter.pylint.allow_dunder_method_names = [] +linter.pylint.max_args = 5 +linter.pylint.max_positional_args = 5 +linter.pylint.max_returns = 6 +linter.pylint.max_bool_expr = 5 +linter.pylint.max_branches = 12 +linter.pylint.max_statements = 50 +linter.pylint.max_public_methods = 20 +linter.pylint.max_locals = 15 +linter.pylint.max_nested_blocks = 5 +linter.pyupgrade.keep_runtime_typing = false +linter.ruff.parenthesize_tuple_in_subscript = false +linter.ruff.strictly_empty_init_modules = false + +# Formatter Settings +formatter.exclude = [] +formatter.unresolved_target_version = 3.10 +formatter.per_file_target_version = {} +formatter.preview = disabled +formatter.line_width = 120 +formatter.line_ending = auto +formatter.indent_style = space +formatter.indent_width = 4 +formatter.quote_style = double +formatter.magic_trailing_comma = respect +formatter.docstring_code_format = disabled +formatter.docstring_code_line_width = dynamic + +# Analyze Settings +analyze.exclude = [] +analyze.preview = disabled +analyze.target_version = 3.10 +analyze.string_imports = disabled +analyze.extension = ExtensionMapping({}) +analyze.include_dependencies = {} +analyze.type_checking_imports = true + +----- stderr ----- diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 1740cc184a..42ad1a5067 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -102,8 +102,8 @@ impl Relativity { #[derive(Debug)] pub struct Resolver<'a> { pyproject_config: &'a PyprojectConfig, - /// All [`Settings`] that have been added to the resolver. - settings: Vec, + /// All [`Settings`] that have been added to the resolver, along with their config file paths. + settings: Vec<(Settings, PathBuf)>, /// A router from path to index into the `settings` vector. router: Router, } @@ -146,8 +146,8 @@ impl<'a> Resolver<'a> { } /// Add a resolved [`Settings`] under a given [`PathBuf`] scope. - fn add(&mut self, path: &Path, settings: Settings) { - self.settings.push(settings); + fn add(&mut self, path: &Path, settings: Settings, config_path: PathBuf) { + self.settings.push((settings, config_path)); // Normalize the path to use `/` separators and escape the '{' and '}' characters, // which matchit uses for routing parameters. @@ -172,13 +172,27 @@ impl<'a> Resolver<'a> { /// Return the appropriate [`Settings`] for a given [`Path`]. pub fn resolve(&self, path: &Path) -> &Settings { + self.resolve_with_path(path).0 + } + + /// Return the appropriate [`Settings`] and config file path for a given [`Path`]. + pub fn resolve_with_path(&self, path: &Path) -> (&Settings, Option<&Path>) { match self.pyproject_config.strategy { - PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings, + PyprojectDiscoveryStrategy::Fixed => ( + &self.pyproject_config.settings, + self.pyproject_config.path.as_deref(), + ), PyprojectDiscoveryStrategy::Hierarchical => self .router .at(path.to_slash_lossy().as_ref()) - .map(|Match { value, .. }| &self.settings[*value]) - .unwrap_or(&self.pyproject_config.settings), + .map(|Match { value, .. }| { + let (settings, config_path) = &self.settings[*value]; + (settings, Some(config_path.as_path())) + }) + .unwrap_or(( + &self.pyproject_config.settings, + self.pyproject_config.path.as_deref(), + )), } } @@ -255,7 +269,8 @@ impl<'a> Resolver<'a> { /// Return an iterator over the resolved [`Settings`] in this [`Resolver`]. pub fn settings(&self) -> impl Iterator { - std::iter::once(&self.pyproject_config.settings).chain(&self.settings) + std::iter::once(&self.pyproject_config.settings) + .chain(self.settings.iter().map(|(settings, _)| settings)) } } @@ -379,17 +394,17 @@ pub fn resolve_configuration( /// Extract the project root (scope) and [`Settings`] from a given /// `pyproject.toml`. -fn resolve_scoped_settings<'a>( - pyproject: &'a Path, +fn resolve_scoped_settings( + pyproject: &Path, transformer: &dyn ConfigurationTransformer, origin: ConfigurationOrigin, -) -> Result<(&'a Path, Settings)> { +) -> Result<(PathBuf, Settings)> { let relativity = Relativity::from(origin); let configuration = resolve_configuration(pyproject, transformer, origin)?; let project_root = relativity.resolve(pyproject); let settings = configuration.into_settings(project_root)?; - Ok((project_root, settings)) + Ok((project_root.to_path_buf(), settings)) } /// Extract the [`Settings`] from a given `pyproject.toml` and process the @@ -455,7 +470,7 @@ pub fn python_files_in_path<'a>( transformer, ConfigurationOrigin::Ancestor, )?; - resolver.add(root, settings); + resolver.add(&root, settings, pyproject); // We found the closest configuration. break; } @@ -647,7 +662,11 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> { ConfigurationOrigin::Ancestor, ) { Ok((root, settings)) => { - self.global.resolver.write().unwrap().add(root, settings); + self.global + .resolver + .write() + .unwrap() + .add(&root, settings, pyproject); } Err(err) => { self.local_error = Err(err); @@ -767,7 +786,7 @@ pub fn python_file_at_path( if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?; - resolver.add(root, settings); + resolver.add(&root, settings, pyproject); break; } }