diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index a408810b2a..47a234d173 100644 --- a/crates/pyrefly_config/src/config.rs +++ b/crates/pyrefly_config/src/config.rs @@ -984,6 +984,256 @@ impl ConfigFile { result } + /// Render every user-facing setting as `label: value` lines for + /// `dump-config --full`, indented relative to the caller's own indentation. + /// + /// This omits what `dump-config` already prints — the config source, the + /// interpreter, the covered files, and everything reported by + /// `structured_import_lookup_path` — so that `--full` adds information + /// rather than repeating it. + pub fn structured_settings(&self) -> Vec { + fn value(value: &Option) -> Option { + value.as_ref().map(T::to_string) + } + + fn path(value: &Option) -> Option { + value.as_ref().map(|p| p.display().to_string()) + } + + // Render an enum by its config-file spelling (`skip-and-infer-return-any`) + // rather than its Rust variant name, so that it matches what a user writes. + fn variant(value: &Option) -> Option { + value.as_ref().map(|v| { + v.to_possible_value() + .expect("config enums do not skip variants") + .get_name() + .to_owned() + }) + } + + fn list(values: impl IntoIterator) -> String { + format!("[{}]", values.into_iter().join(", ")) + } + + fn or_unset(value: Option) -> String { + value.unwrap_or_else(|| "(unset)".to_owned()) + } + + // Every setting a `ConfigBase` carries, labelled by its config-file key so + // that it can be matched against what the user wrote. A `None` value means + // the setting is unspecified, which for a sub-config means "not overridden". + fn base_settings(base: &ConfigBase) -> Vec<(&'static str, Option)> { + vec![ + ( + "check-unannotated-defs", + value(&base.check_unannotated_defs), + ), + ("infer-return-types", variant(&base.infer_return_types)), + ("infer-with-first-use", value(&base.infer_with_first_use)), + ("check-all-matches", value(&base.check_all_matches)), + ( + "strict-callable-subtyping", + value(&base.strict_callable_subtyping), + ), + ( + "strict-partial-subtyping", + value(&base.strict_partial_subtyping), + ), + ( + "spec-compliant-overloads", + value(&base.spec_compliant_overloads), + ), + ( + "legacy-overload-expansion", + value(&base.legacy_overload_expansion), + ), + ( + "treat-all-caps-as-final", + value(&base.treat_all_caps_as_final), + ), + ( + "disable-type-errors-in-ide", + value(&base.disable_type_errors_in_ide), + ), + ( + "ignore-errors-in-generated-code", + value(&base.ignore_errors_in_generated_code), + ), + ("permissive-ignores", value(&base.permissive_ignores)), + ( + "enabled-ignores", + base.enabled_ignores.as_ref().map(|tools| { + list(tools.iter().map(|t| { + t.to_possible_value() + .expect("Tool does not skip variants") + .get_name() + .to_owned() + })) + }), + ), + ( + "replace-imports-with-any", + base.replace_imports_with_any.as_ref().map(list), + ), + ( + "ignore-missing-imports", + base.ignore_missing_imports.as_ref().map(list), + ), + ("recursion-depth-limit", value(&base.recursion_depth_limit)), + ( + "recursion-overflow-handler", + variant(&base.recursion_overflow_handler), + ), + ( + "untyped-def-behavior (deprecated)", + variant(&base.untyped_def_behavior), + ), + ( + "pytorch-efficiency-lints (deprecated)", + value(&base.pytorch_efficiency_lints), + ), + ] + } + + let mut lines = vec![ + format!( + "Required version: {}", + or_unset(value(&self.required_version)) + ), + "Project files:".to_owned(), + format!(" Includes: {}", self.project_includes), + format!(" Excludes: {}", self.project_excludes), + format!( + " Excludes heuristics disabled: {}", + self.disable_project_excludes_heuristics + ), + format!(" Respecting ignore files: {}", self.use_ignore_files), + format!( + " Extra file extensions: {}", + list(&self.extra_file_extensions) + ), + "Python environment:".to_owned(), + format!( + " Version: {}", + or_unset(value(&self.python_environment.python_version)) + ), + format!( + " Platform: {}", + or_unset(value(&self.python_environment.python_platform)) + ), + "Import settings:".to_owned(), + format!( + " Search path heuristics disabled: {}", + self.disable_search_path_heuristics + ), + format!( + " Fallback search path enabled: {}", + self.enable_fallback_search_path + ), + format!(" Typeshed path: {}", or_unset(path(&self.typeshed_path))), + "Reporting:".to_owned(), + format!( + " Output format: {}", + or_unset(variant(&self.output_format)) + ), + format!( + " Minimum severity: {}", + or_unset(variant(&self.min_severity)) + ), + format!(" Baseline: {}", or_unset(path(&self.baseline))), + format!( + " Baseline error level: {}", + or_unset(variant(&self.baseline_error_level)) + ), + format!( + " Skip LSP config indexing: {}", + self.skip_lsp_config_indexing + ), + "Type checking:".to_owned(), + format!(" Preset: {}", or_unset(variant(&self.preset))), + ]; + lines.extend( + base_settings(&self.root) + .into_iter() + .map(|(key, setting)| format!(" {key}: {}", or_unset(setting))), + ); + + // `ErrorDisplayConfig` is backed by a `HashMap`, so sort to keep the output + // stable from run to run. + let errors = self + .root + .errors + .iter() + .flat_map(|errors| errors.iter()) + .map(|(kind, severity)| (kind.to_name(), severity)) + .sorted(); + lines.push("Error severity overrides:".to_owned()); + lines.extend(errors.map(|(kind, severity)| { + format!( + " {kind}: {}", + severity + .to_possible_value() + .expect("Severity does not skip variants") + .get_name() + ) + })); + + lines.push("Path-scoped overrides:".to_owned()); + for sub in &self.sub_configs { + lines.push(format!(" Matching `{}`:", sub.matches)); + lines.extend( + base_settings(&sub.settings) + .into_iter() + .filter_map(|(key, setting)| { + setting.map(|setting| format!(" {key}: {setting}")) + }), + ); + } + + lines.push("Coverage overrides:".to_owned()); + lines.push(format!( + " Includes: {}", + or_unset(value(&self.coverage.includes)) + )); + lines.push(format!( + " Excludes: {}", + or_unset(value(&self.coverage.excludes)) + )); + + lines.push("Build system:".to_owned()); + match &self.build_system { + None => lines.push(" (none)".to_owned()), + Some(build_system) => { + lines.push(format!(" Args: {}", build_system.args)); + lines.push(format!( + " Ignore if missing: {}", + build_system.ignore_if_build_system_missing + )); + lines.push(format!( + " Search path prefix: {}", + list(build_system.search_path_prefix.iter().map(|p| p.display())) + )); + lines.push(format!( + " Catch-all targets: {}", + list(&build_system.catch_all_targets) + )); + lines.push(format!( + " Catch-all targets only: {}", + build_system.catch_all_targets_only + )); + } + } + + // Unrecognized keys are silently ignored when a config is parsed, so + // surfacing them here is often the fastest way to spot a typo. + lines.push(format!( + "Unrecognized config keys: {}", + list(self.root.extras.0.keys()) + )); + + lines + } + pub fn get_sys_info(&self) -> SysInfo { SysInfo::new(self.python_version(), self.python_platform().clone()) } diff --git a/crates/pyrefly_config/src/module_wildcard.rs b/crates/pyrefly_config/src/module_wildcard.rs index 14cfb3cae9..49fa7e5776 100644 --- a/crates/pyrefly_config/src/module_wildcard.rs +++ b/crates/pyrefly_config/src/module_wildcard.rs @@ -109,6 +109,12 @@ impl<'de> Deserialize<'de> for ModuleWildcard { } } +impl fmt::Display for ModuleWildcard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.origin) + } +} + impl Serialize for ModuleWildcard { fn serialize(&self, serializer: S) -> Result where diff --git a/pyrefly/lib/commands/dump_config.rs b/pyrefly/lib/commands/dump_config.rs index a33fb817cb..794b4903b6 100644 --- a/pyrefly/lib/commands/dump_config.rs +++ b/pyrefly/lib/commands/dump_config.rs @@ -78,6 +78,10 @@ pub struct DumpConfigArgs { /// config(s) to print. Pass "all" to output all files. #[arg(long, default_value_t = MaxFiles::Count(10))] max_files: MaxFiles, + /// Print every resolved configuration setting, not just the interpreter + /// and the paths imports are resolved from. + #[arg(long)] + full: bool, #[command(flatten)] args: FullCheckArgs, } @@ -91,6 +95,7 @@ impl DumpConfigArgs { self.args.files, self.args.config_override, self.max_files, + self.full, wrapper, ) } @@ -100,6 +105,7 @@ fn dump_config( files: FilesArgs, config_override: ConfigOverrideArgs, max_files: MaxFiles, + full: bool, wrapper: Option, ) -> anyhow::Result { config_override.validate()?; @@ -147,6 +153,11 @@ fn dump_config( } } println!(" Using interpreter: {}", config.interpreters); + if full { + for line in config.structured_settings() { + println!(" {line}"); + } + } println!(" Covered files:"); for (i, fi) in files.iter().enumerate() { if max_files.should_print(i) { diff --git a/test/config.md b/test/config.md index 8c92c322d1..add7dc4cae 100644 --- a/test/config.md +++ b/test/config.md @@ -50,6 +50,58 @@ Default configuration [0] ``` +## Dump config with --full + + + +```scrut {output_stream: stdout} +$ mkdir -p $TMPDIR/full/src && touch $TMPDIR/full/src/a.py && \ +> printf 'project-includes = ["src/**/*.py"]\npython-version = "3.12.0"\nuse-ignore-files = false\nnot-a-real-key = 1\n\n[errors]\nmissing-import = "warn"\nbad-assignment = "error"\n\n[[sub-config]]\nmatches = "src/**"\ncheck-unannotated-defs = false\n' > $TMPDIR/full/pyrefly.toml && \ +> $PYREFLY dump-config --full $TMPDIR/full/src/a.py +Configuration at `*/full/pyrefly.toml` (glob) + Using interpreter: * (glob) + Required version: (unset) + Project files: + Includes: [*/full/src/**/*.py] (glob) + Excludes: * (glob) + Excludes heuristics disabled: false + Respecting ignore files: false + Extra file extensions: [] + Python environment: + Version: 3.12.0 + Platform: * (glob) + Import settings: + Search path heuristics disabled: false + Fallback search path enabled: false + Typeshed path: (unset) + Reporting: + Output format: (unset) + Minimum severity: (unset) + Baseline: (unset) + Baseline error level: (unset) + Skip LSP config indexing: false + Type checking: +* (glob+) + Error severity overrides: + bad-assignment: error + missing-import: warn + Path-scoped overrides: + Matching `*/full/src/**/*`: (glob) + check-unannotated-defs: false + Coverage overrides: + Includes: (unset) + Excludes: (unset) + Build system: + (none) + Unrecognized config keys: [not-a-real-key] + Covered files: + */full/src/a.py (glob) + Resolving imports from: +* (glob+) +[0] +``` + ## Specify both files and config ```scrut {output_stream: stderr} diff --git a/website/docs/import-resolution.mdx b/website/docs/import-resolution.mdx index 73e2250e19..4185d6e96c 100644 --- a/website/docs/import-resolution.mdx +++ b/website/docs/import-resolution.mdx @@ -214,3 +214,8 @@ It will only use import hooks if you set Pyrefly has a `dump-config` command that dumps the import-related config options it is using for each file it is checking. To use it, simply replace `check` with `dump-config` in your command-line invocation. + +Pass `--full` to also print every other resolved configuration setting, including error +severity overrides, path-scoped `sub-config` overrides, and any config keys Pyrefly did not +recognize. This is useful when a setting does not appear to take effect and you need to see +the value Pyrefly actually resolved.