diff --git a/pyrefly/lib/commands/check.rs b/pyrefly/lib/commands/check.rs index e54c05cf77..a92f273ea5 100644 --- a/pyrefly/lib/commands/check.rs +++ b/pyrefly/lib/commands/check.rs @@ -77,6 +77,7 @@ use crate::error::summarize::print_error_summary; use crate::error::suppress; use crate::error::suppress::CommentLocation; use crate::error::suppress::SerializedError; +use crate::error::suppress::UnusedIgnoreKind; use crate::module::typeshed::stdlib_search_path; use crate::report; use crate::state::load::FileContents; @@ -232,7 +233,7 @@ impl SnippetCheckArgs { check_all: false, suppress_errors: false, expectations: false, - remove_unused_ignores: false, + remove_unused_ignores: None, }, }; let (status, check_result) = @@ -417,9 +418,17 @@ struct BehaviorArgs { /// Check against any `E:` lines in the file. #[arg(long)] expectations: bool, - /// Remove unused ignores from the input files. - #[arg(long)] - remove_unused_ignores: bool, + /// Remove unused ignores from the input files, optionally selecting `pyrefly`, `type`, or `all`. + /// Defaults to `pyrefly` when no kind is specified. + #[arg( + long, + value_enum, + value_name = "KIND", + num_args = 0..=1, + require_equals = true, + default_missing_value = "pyrefly" + )] + remove_unused_ignores: Option, } fn write_errors_to_file( @@ -1315,11 +1324,11 @@ impl CheckArgs { .collect(); suppress::suppress_errors(serialized_errors, CommentLocation::LineBefore); } - if self.behavior.remove_unused_ignores { + if let Some(kind) = self.behavior.remove_unused_ignores { // TODO: Deprecate this in favor of `pyrefly suppress` let collected = loads.collect_errors(); let unused_errors = loads.collect_unused_ignore_errors(&collected); - suppress::remove_unused_ignores(unused_errors); + suppress::remove_unused_ignores(unused_errors, kind); } // We update the baseline file if requested, after reporting any new @@ -1702,6 +1711,35 @@ mod tests { assert_eq!(output.output_format(), OutputFormat::Json); } + #[test] + fn remove_unused_ignores_cli_values() { + for (argument, expected) in [ + (None, None), + ( + Some("--remove-unused-ignores"), + Some(UnusedIgnoreKind::Pyrefly), + ), + ( + Some("--remove-unused-ignores=pyrefly"), + Some(UnusedIgnoreKind::Pyrefly), + ), + ( + Some("--remove-unused-ignores=type"), + Some(UnusedIgnoreKind::Type), + ), + ( + Some("--remove-unused-ignores=all"), + Some(UnusedIgnoreKind::All), + ), + ] { + let args = argument.map_or_else( + || CheckArgs::parse_from(["check"]), + |argument| CheckArgs::parse_from(["check", argument]), + ); + assert_eq!(args.behavior.remove_unused_ignores, expected); + } + } + fn upsell_string(reason: SynthesizedPresetReason) -> String { let mut buf = Vec::new(); write_unconfigured_upsell(reason, &mut buf).unwrap(); diff --git a/pyrefly/lib/commands/suppress.rs b/pyrefly/lib/commands/suppress.rs index b04a7898a8..2320ab04c8 100644 --- a/pyrefly/lib/commands/suppress.rs +++ b/pyrefly/lib/commands/suppress.rs @@ -18,6 +18,7 @@ use crate::commands::util::CommandExitStatus; use crate::error::suppress; use crate::error::suppress::CommentLocation; use crate::error::suppress::SerializedError; +use crate::error::suppress::UnusedIgnoreKind; /// Suppress type errors by adding ignore comments to source files. #[derive(Clone, Debug, Parser)] @@ -35,9 +36,17 @@ pub struct SuppressArgs { #[arg(long)] json: Option, - /// Remove unused ignore comments instead of adding suppressions. - #[arg(long)] - remove_unused: bool, + /// Remove unused ignores instead of adding suppressions, optionally selecting `pyrefly`, `type`, or `all`. + /// Defaults to `pyrefly` when no kind is specified. + #[arg( + long, + value_enum, + value_name = "KIND", + num_args = 0..=1, + require_equals = true, + default_missing_value = "pyrefly" + )] + remove_unused: Option, /// Where to place suppression comments: on the line before the error /// (`line-before`, the default) or on the same line (`same-line`). @@ -51,19 +60,22 @@ impl SuppressArgs { wrapper: Option, thread_count: ThreadCount, ) -> anyhow::Result { - if self.remove_unused { + if let Some(kind) = self.remove_unused { // Remove unused ignores mode let unused_errors: Vec = if let Some(json_path) = &self.json { - // Parse errors from JSON file, filtering for UnusedIgnore errors only + // Parse errors from JSON file, filtering for unused suppression errors only. let json_content = std::fs::read_to_string(json_path)?; let errors: Vec = serde_json::from_str(&json_content)?; errors .into_iter() - .filter(|e| e.is_unused_ignore()) + .filter(|e| { + kind.includes_pyrefly_or_pyre() && e.is_unused_ignore() + || kind.includes_type() && e.is_unused_type_ignore() + }) .collect() } else { - // Delegate to `check --remove-unused-ignores`, which calls - // collect_unused_ignore_errors directly (bypassing severity + // Delegate to `check --remove-unused-ignores`, which + // collects unused ignore errors directly (bypassing severity // filtering) and handles removal in one step. self.config_override.validate()?; let (files_to_check, config_finder, upsell) = self @@ -71,18 +83,23 @@ impl SuppressArgs { .clone() .resolve(self.config_override.clone(), wrapper.clone())?; + let remove_unused_flag = match kind { + UnusedIgnoreKind::Pyrefly => "--remove-unused-ignores=pyrefly", + UnusedIgnoreKind::Type => "--remove-unused-ignores=type", + UnusedIgnoreKind::All => "--remove-unused-ignores=all", + }; let check_args = CheckArgs::parse_from([ "check", "--output-format", "omit-errors", - "--remove-unused-ignores", + remove_unused_flag, ]); check_args.run_once(files_to_check, config_finder, upsell, thread_count)?; return Ok(CommandExitStatus::Success); }; // Remove unused ignores (JSON path only) - suppress::remove_unused_ignores_from_serialized(unused_errors); + suppress::remove_unused_ignores_from_serialized(unused_errors, kind); } else { // Add suppressions mode (existing behavior) let serialized_errors: Vec = if let Some(json_path) = &self.json { @@ -122,3 +139,28 @@ impl SuppressArgs { Ok(CommandExitStatus::Success) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remove_unused_cli_values() { + for (argument, expected) in [ + (None, None), + (Some("--remove-unused"), Some(UnusedIgnoreKind::Pyrefly)), + ( + Some("--remove-unused=pyrefly"), + Some(UnusedIgnoreKind::Pyrefly), + ), + (Some("--remove-unused=type"), Some(UnusedIgnoreKind::Type)), + (Some("--remove-unused=all"), Some(UnusedIgnoreKind::All)), + ] { + let args = argument.map_or_else( + || SuppressArgs::parse_from(["suppress"]), + |argument| SuppressArgs::parse_from(["suppress", argument]), + ); + assert_eq!(args.remove_unused, expected); + } + } +} diff --git a/pyrefly/lib/error/suppress.rs b/pyrefly/lib/error/suppress.rs index 857d08e9d8..ad10d38d12 100644 --- a/pyrefly/lib/error/suppress.rs +++ b/pyrefly/lib/error/suppress.rs @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +use std::borrow::Cow; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; @@ -38,13 +39,17 @@ use crate::state::errors::sorted_backslash_continuation_ranges; use crate::state::errors::sorted_bracketed_continuation_ranges; use crate::state::errors::sorted_multi_line_string_ranges; -/// Regex to match pyrefly/type/pyre ignore comments with optional error codes and trailing text. -/// Consumes all non-`#` characters after the ignore pattern, so trailing comment text is -/// removed, but a separate `# ...` comment is preserved -/// (e.g., "# pyrefly: ignore [x] # other" -> "# other"). -static IGNORE_COMMENT_REGEX: LazyLock = LazyLock::new(|| { +/// Regexes to match ignore comments with optional error codes and trailing text. +/// Each consumes all non-`#` characters after its ignore pattern, so removing one +/// suppression preserves any other pragma later on the same line. +static PYREFLY_IGNORE_COMMENT_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"#\s*pyrefly:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*").unwrap() +}); +static TYPE_IGNORE_COMMENT_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"#\s*type:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*").unwrap()); +static PYRE_IGNORE_COMMENT_REGEX: LazyLock = LazyLock::new(|| { Regex::new( - r"#\s*pyrefly:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*|#\s*type:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*|#\s*pyre-(?:fixme|ignore)\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*|#\s*pyre:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*", + r"#\s*pyre-(?:fixme|ignore)\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*|#\s*pyre:\s*ignore\s*(\[[^\]]*\])?\s*(?:;\s*)?[^#]*", ) .unwrap() }); @@ -59,6 +64,30 @@ pub enum CommentLocation { SameLine, } +/// Which kinds of unused ignore comments to remove. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] +pub enum UnusedIgnoreKind { + /// Remove unused Pyrefly and Pyre ignores (default). + #[default] + Pyrefly, + /// Remove unused `# type: ignore` comments. + Type, + /// Remove unused Pyrefly, Pyre, and `# type: ignore` comments. + All, +} + +impl UnusedIgnoreKind { + /// Whether unused Pyrefly and Pyre ignores should be removed. + pub fn includes_pyrefly_or_pyre(self) -> bool { + matches!(self, Self::Pyrefly | Self::All) + } + + /// Whether unused `# type: ignore` comments should be removed. + pub fn includes_type(self) -> bool { + matches!(self, Self::Type | Self::All) + } +} + /// A serializable representation of an error for JSON input/output. /// This struct holds the fields needed to add or remove a suppression comment. #[derive(Deserialize, Serialize)] @@ -98,6 +127,11 @@ impl SerializedError { self.name == ErrorKind::UnusedIgnore.to_name() } + /// Returns true if this error is an UnusedTypeIgnore error. + pub fn is_unused_type_ignore(&self) -> bool { + self.name == ErrorKind::UnusedTypeIgnore.to_name() + } + /// Returns true if this error is a directive (e.g. reveal_type) that /// should never be suppressed. pub fn is_directive(&self) -> bool { @@ -519,8 +553,8 @@ fn update_ignore_comment_with_used_codes( // If there are no used codes, remove the entire comment if used_codes.is_empty() { - if IGNORE_COMMENT_REGEX.is_match(comment_part) { - let new_comment = IGNORE_COMMENT_REGEX.replace_all(comment_part, ""); + if PYREFLY_IGNORE_COMMENT_REGEX.is_match(comment_part) { + let new_comment = PYREFLY_IGNORE_COMMENT_REGEX.replace(comment_part, ""); let result = format!("{}{}", code_part, new_comment); return Some(result.trim_end().to_owned()); } @@ -553,20 +587,24 @@ fn update_ignore_comment_with_used_codes( /// Takes a list of UnusedIgnore errors (from collect_unused_ignore_errors) and uses /// the error location and message to determine what to remove: /// - "Unused `# pyrefly: ignore` comment" -> remove entire comment +/// - "Unused `# type: ignore` comment" -> remove entire comment when opted in /// - "Unused `# pyrefly: ignore` comment for code(s): X, Y" -> remove entire comment /// - "Unused error code(s) in `# pyrefly: ignore`: X, Y" -> remove only those codes -pub fn remove_unused_ignores(unused_ignore_errors: Vec) -> usize { +pub fn remove_unused_ignores(unused_ignore_errors: Vec, kind: UnusedIgnoreKind) -> usize { let serialized: Vec = unused_ignore_errors .iter() .filter_map(SerializedError::from_error) .collect(); - remove_unused_ignores_from_serialized(serialized) + remove_unused_ignores_from_serialized(serialized, kind) } /// Removes unused ignore comments from source files using SerializedError. /// This is similar to remove_unused_ignores but works with SerializedError instead of Error, /// allowing it to be used with errors parsed from JSON. -pub fn remove_unused_ignores_from_serialized(unused_ignore_errors: Vec) -> usize { +pub fn remove_unused_ignores_from_serialized( + unused_ignore_errors: Vec, + kind: UnusedIgnoreKind, +) -> usize { if unused_ignore_errors.is_empty() { return 0; } @@ -574,6 +612,11 @@ pub fn remove_unused_ignores_from_serialized(unused_ignore_errors: Vec> = SmallMap::new(); for error in &unused_ignore_errors { + if !((kind.includes_pyrefly_or_pyre() && error.is_unused_ignore()) + || (kind.includes_type() && error.is_unused_type_ignore())) + { + continue; + } errors_by_path .entry(error.path.clone()) .or_default() @@ -583,10 +626,11 @@ pub fn remove_unused_ignores_from_serialized(unused_ignore_errors: Vec = SmallMap::new(); for (path, path_errors) in &errors_by_path { - // Build a map from line number to the error - let mut line_errors: SmallMap = SmallMap::new(); + // Build a map from line number to its errors. Multiple unused suppressions + // may appear on the same line and must each be handled independently. + let mut line_errors: SmallMap> = SmallMap::new(); for error in path_errors { - line_errors.insert(error.line, *error); + line_errors.entry(error.line).or_default().push(*error); } if let Ok((file, _ast)) = read_and_validate_file(path) { @@ -596,63 +640,73 @@ pub fn remove_unused_ignores_from_serialized(unused_ignore_errors: Vec = codes_part - .split(", ") - .map(|s| s.trim().to_owned()) + if let Some(errors) = line_errors.get(&idx) { + let mut updated_line = Cow::Borrowed(*line); + + for error in errors { + let msg = &error.message; + + if msg.starts_with("Unused error code(s)") { + // Partially unused - extract codes from message and remove only those. + // Message format: "Unused error code(s) in `# pyrefly: ignore`: code1, code2" + if let Some(codes_part) = msg.split(": ").last() { + let unused_codes: SmallSet = codes_part + .split(", ") + .map(|s| s.trim().to_owned()) + .collect(); + + if let Some(existing_codes) = parse_ignore_comment(&updated_line) { + let used_codes: SmallSet = existing_codes + .into_iter() + .filter(|c| !unused_codes.contains(c)) .collect(); - if let Some(existing_codes) = parse_ignore_comment(line) { - let used_codes: SmallSet = existing_codes - .into_iter() - .filter(|c| !unused_codes.contains(c)) - .collect(); - - if let Some(updated) = update_ignore_comment_with_used_codes( - line, - &used_codes, - &unused_codes, - ) { - unused_count += 1; - if !updated.trim().is_empty() { - buf.push_str(&updated); - buf.push_str(line_ending); - } - continue; - } + if let Some(updated) = update_ignore_comment_with_used_codes( + &updated_line, + &used_codes, + &unused_codes, + ) { + updated_line = Cow::Owned(updated); + unused_count += 1; } } } + continue; + } + + let ignore_regex = if error.is_unused_type_ignore() { + &*TYPE_IGNORE_COMMENT_REGEX + } else if msg.starts_with("Unused `# pyrefly: ignore` comment") { + &*PYREFLY_IGNORE_COMMENT_REGEX + } else if msg.starts_with("Unused pyre-fixme comment") { + &*PYRE_IGNORE_COMMENT_REGEX + } else { + continue; + }; + + // Use string-aware comment detection instead of raw regex. + let Some(comment_start) = find_comment_start_in_line(&updated_line) else { + continue; + }; + let comment_part = &updated_line[comment_start..]; + if let Cow::Owned(new_comment) = ignore_regex.replace(comment_part, "") { + let code_part = &updated_line[..comment_start]; + updated_line = Cow::Owned( + format!("{}{}", code_part, new_comment) + .trim_end() + .to_owned(), + ); + unused_count += 1; } } + + if let Cow::Owned(updated_line) = updated_line { + if !updated_line.trim().is_empty() { + buf.push_str(&updated_line); + buf.push_str(line_ending); + } + continue; + } } buf.push_str(line); buf.push_str(line_ending); @@ -734,7 +788,7 @@ mod tests { let (errors, tdir) = get_errors(before); let collected = errors.collect_errors(); let unused_errors = errors.collect_unused_ignore_errors(&collected); - let removals = suppress::remove_unused_ignores(unused_errors); + let removals = suppress::remove_unused_ignores(unused_errors, UnusedIgnoreKind::Pyrefly); let got_file = fs_anyhow::read_to_string(&get_path(&tdir)).unwrap(); assert_eq!(after, got_file); assert_eq!(removals, expected_removals); @@ -751,7 +805,7 @@ mod tests { let (errors, tdir) = get_errors(before); let collected = errors.collect_errors(); let unused_errors = errors.collect_unused_ignore_errors(&collected); - let removals = suppress::remove_unused_ignores(unused_errors); + let removals = suppress::remove_unused_ignores(unused_errors, UnusedIgnoreKind::Pyrefly); let got_file = fs_anyhow::read_to_string(&get_path(&tdir)).unwrap(); assert_eq!(after, got_file); assert_eq!(removals, expected_removals); @@ -1140,6 +1194,21 @@ def f() -> int: assert_remove_ignores(input, output, 2); } + #[test] + fn test_remove_unused_type_ignore_when_enabled() { + let input = "a = 1 # pyrefly: ignore\nb = 2 # type: ignore\n"; + let want = "a = 1\nb = 2\n"; + let (errors, tdir) = get_errors(input); + let collected = errors.collect_errors(); + let unused_errors = errors.collect_unused_ignore_errors(&collected); + + let removals = suppress::remove_unused_ignores(unused_errors, UnusedIgnoreKind::All); + + let got_file = fs_anyhow::read_to_string(&get_path(&tdir)).unwrap(); + assert_eq!(want, got_file); + assert_eq!(removals, 2); + } + #[test] fn test_errors_deduped() { let file_contents = r#" @@ -1436,10 +1505,26 @@ def f() -> int: // Helper function to test remove_unused_ignores_from_serialized fn assert_remove_ignores_from_serialized( + file_content: &str, + serialized_errors: Vec, + expected_content: &str, + expected_removals: usize, + ) { + assert_remove_ignores_from_serialized_with_kind( + file_content, + serialized_errors, + expected_content, + expected_removals, + UnusedIgnoreKind::Pyrefly, + ); + } + + fn assert_remove_ignores_from_serialized_with_kind( file_content: &str, mut serialized_errors: Vec, expected_content: &str, expected_removals: usize, + kind: UnusedIgnoreKind, ) { let tdir = tempfile::tempdir().unwrap(); let path = get_path(&tdir); @@ -1450,13 +1535,111 @@ def f() -> int: error.path = path.clone(); } - let removals = suppress::remove_unused_ignores_from_serialized(serialized_errors); + let removals = suppress::remove_unused_ignores_from_serialized(serialized_errors, kind); let got_file = fs_anyhow::read_to_string(&path).unwrap(); assert_eq!(expected_content, got_file); assert_eq!(removals, expected_removals); } + #[test] + fn test_used_type_ignore_preserved_when_removing_pyrefly_ignore() { + // An unused Pyrefly suppression must not remove a still-used type suppression + // from the same line when type-ignore removal was not requested. + let input = "x = 1 # pyrefly: ignore # type: ignore\n"; + let want = "x = 1 # type: ignore\n"; + let errors = vec![SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-ignore".to_owned(), + message: "Unused `# pyrefly: ignore` comment".to_owned(), + }]; + assert_remove_ignores_from_serialized_with_kind( + input, + errors, + want, + 1, + UnusedIgnoreKind::Pyrefly, + ); + } + + #[test] + fn test_used_pyrefly_ignore_preserved_when_removing_type_ignore() { + // An unused type suppression must not remove a still-used Pyrefly suppression + // from the same line when type-ignore removal was requested. + let input = "x = 1 # type: ignore # pyrefly: ignore\n"; + let want = "x = 1 # pyrefly: ignore\n"; + let errors = vec![SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-type-ignore".to_owned(), + message: "Unused `# type: ignore` comment".to_owned(), + }]; + assert_remove_ignores_from_serialized_with_kind( + input, + errors, + want, + 1, + UnusedIgnoreKind::Type, + ); + } + + #[test] + fn test_remove_only_selected_suppression_kind() { + let input = "x = 1 # pyrefly: ignore # type: ignore\n"; + for (kind, want) in [ + (UnusedIgnoreKind::Pyrefly, "x = 1 # type: ignore\n"), + (UnusedIgnoreKind::Type, "x = 1 # pyrefly: ignore\n"), + ] { + let errors = vec![ + SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-ignore".to_owned(), + message: "Unused `# pyrefly: ignore` comment".to_owned(), + }, + SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-type-ignore".to_owned(), + message: "Unused `# type: ignore` comment".to_owned(), + }, + ]; + assert_remove_ignores_from_serialized_with_kind(input, errors, want, 1, kind); + } + } + + #[test] + fn test_remove_multiple_unused_suppressions_from_same_line() { + let want = "x = 1\n"; + for input in [ + "x = 1 # pyrefly: ignore # type: ignore\n", + "x = 1 # type: ignore # pyrefly: ignore\n", + ] { + let errors = vec![ + SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-ignore".to_owned(), + message: "Unused `# pyrefly: ignore` comment".to_owned(), + }, + SerializedError { + path: PathBuf::from("test.py"), + line: 0, + name: "unused-type-ignore".to_owned(), + message: "Unused `# type: ignore` comment".to_owned(), + }, + ]; + assert_remove_ignores_from_serialized_with_kind( + input, + errors, + want, + 2, + UnusedIgnoreKind::All, + ); + } + } + #[test] fn test_remove_unused_ignores_from_serialized_blanket() { let input = r#"def g() -> str: @@ -1560,7 +1743,8 @@ def g() -> str: }, ]; - let removals = suppress::remove_unused_ignores_from_serialized(errors); + let removals = + suppress::remove_unused_ignores_from_serialized(errors, UnusedIgnoreKind::Pyrefly); assert_eq!(fs_anyhow::read_to_string(&path1).unwrap(), "x = 1\n"); assert_eq!(fs_anyhow::read_to_string(&path2).unwrap(), "y = 2\n"); @@ -1570,7 +1754,8 @@ def g() -> str: #[test] fn test_remove_unused_ignores_from_serialized_empty_list() { let errors: Vec = vec![]; - let removals = suppress::remove_unused_ignores_from_serialized(errors); + let removals = + suppress::remove_unused_ignores_from_serialized(errors, UnusedIgnoreKind::Pyrefly); assert_eq!(removals, 0); } @@ -2074,7 +2259,8 @@ build_query( name: "unused-ignore".to_owned(), message: "Unused pyre-fixme comment".to_owned(), }]; - let removals = suppress::remove_unused_ignores_from_serialized(errors); + let removals = + suppress::remove_unused_ignores_from_serialized(errors, UnusedIgnoreKind::Pyrefly); let got = fs_anyhow::read_to_string(&path).unwrap(); assert_eq!(want, got); assert_eq!(removals, 1); diff --git a/test/suppress.md b/test/suppress.md index 86588336b1..2d8689165d 100644 --- a/test/suppress.md +++ b/test/suppress.md @@ -1,5 +1,53 @@ # Tests for `--suppress-errors` +## `suppress --remove-unused` preserves unused `# type: ignore` comments by default + +```scrut +$ mkdir $TMPDIR/suppress_remove_unused_default && \ +> printf 'a = 1 # pyrefly: ignore\nb = 2 # type: ignore\n' > $TMPDIR/suppress_remove_unused_default/main.py && \ +> : > $TMPDIR/suppress_remove_unused_default/pyrefly.toml && \ +> $PYREFLY suppress $TMPDIR/suppress_remove_unused_default/main.py \ +> --config $TMPDIR/suppress_remove_unused_default/pyrefly.toml \ +> --remove-unused \ +> >/dev/null 2>/dev/null && \ +> cat $TMPDIR/suppress_remove_unused_default/main.py +a = 1 +b = 2 # type: ignore +[0] +``` + +## `suppress --remove-unused=type` removes only unused `# type: ignore` comments + +```scrut +$ mkdir $TMPDIR/suppress_remove_unused_type && \ +> printf 'a = 1 # pyrefly: ignore\nb = 2 # type: ignore\n' > $TMPDIR/suppress_remove_unused_type/main.py && \ +> : > $TMPDIR/suppress_remove_unused_type/pyrefly.toml && \ +> $PYREFLY suppress $TMPDIR/suppress_remove_unused_type/main.py \ +> --config $TMPDIR/suppress_remove_unused_type/pyrefly.toml \ +> --remove-unused=type \ +> >/dev/null 2>/dev/null && \ +> cat $TMPDIR/suppress_remove_unused_type/main.py +a = 1 # pyrefly: ignore +b = 2 +[0] +``` + +## `suppress --remove-unused=all` removes both kinds of unused ignore + +```scrut +$ mkdir $TMPDIR/suppress_remove_unused_all && \ +> printf 'a = 1 # pyrefly: ignore\nb = 2 # type: ignore\n' > $TMPDIR/suppress_remove_unused_all/main.py && \ +> : > $TMPDIR/suppress_remove_unused_all/pyrefly.toml && \ +> $PYREFLY suppress $TMPDIR/suppress_remove_unused_all/main.py \ +> --config $TMPDIR/suppress_remove_unused_all/pyrefly.toml \ +> --remove-unused=all \ +> >/dev/null 2>/dev/null && \ +> cat $TMPDIR/suppress_remove_unused_all/main.py +a = 1 +b = 2 +[0] +``` + ## `--suppress-errors` should not rewrite warnings hidden by `--min-severity` Use an explicit empty config so the repro does not depend on any ancestor diff --git a/website/docs/error-suppressions.mdx b/website/docs/error-suppressions.mdx index aad0cfa36d..19bd7e59b9 100644 --- a/website/docs/error-suppressions.mdx +++ b/website/docs/error-suppressions.mdx @@ -156,10 +156,12 @@ Repeat the steps above until you get a clean formatting run and a clean type che This will add ` # pyrefly: ignore` comments to your code that will enable you to silence errors, and come back and fix them at a later date. This can make the process of upgrading a large codebase much more manageable. +By default, `--remove-unused` removes Pyrefly and Pyre ignores and preserves `# type: ignore` comments because they may be shared with other type checkers. `--remove-unused=pyrefly` is equivalent to the bare flag. Use `pyrefly suppress --remove-unused=type` to remove only unused `# type: ignore` comments, or `pyrefly suppress --remove-unused=all` to remove all three kinds. + :::tip If your project uses other tools that place suppression comments on the line before the error (e.g. other type checkers or linters), use `pyrefly suppress --comment-location=same-line` in step 1 to avoid conflicts. ::: :::note -`pyrefly suppress` is equivalent to `pyrefly check --suppress-errors`, and `pyrefly suppress --remove-unused` is equivalent to `pyrefly check --remove-unused-ignores`. +`pyrefly suppress` is equivalent to `pyrefly check --suppress-errors`, and `pyrefly suppress --remove-unused[=KIND]` is equivalent to `pyrefly check --remove-unused-ignores[=KIND]`. :::