Skip to content

Commit 67e5b08

Browse files
maggiemossmeta-codesync[bot]
authored andcommitted
Add remove-unused to the suppress command
Summary: Adds additional flag so the new suppress command can also remove unused ignores Reviewed By: yangdanny97, ndmitchell Differential Revision: D91707869 fbshipit-source-id: 06a3d5ad627ce32af7cbd9b5f66c3eec3372d998
1 parent 7f0e445 commit 67e5b08

File tree

2 files changed

+240
-40
lines changed

2 files changed

+240
-40
lines changed

pyrefly/lib/commands/suppress.rs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,82 @@ pub struct SuppressArgs {
3131
/// The JSON should be an array of objects with "path", "line", "name", and "message" fields.
3232
#[arg(long)]
3333
json: Option<PathBuf>,
34+
35+
/// Remove unused ignore comments instead of adding suppressions.
36+
#[arg(long)]
37+
remove_unused: bool,
3438
}
3539

3640
impl SuppressArgs {
3741
pub fn run(&self) -> anyhow::Result<CommandExitStatus> {
38-
let suppressable_errors: Vec<SerializedError> = if let Some(json_path) = &self.json {
39-
// Parse errors from JSON file, filtering out UnusedIgnore errors
40-
let json_content = std::fs::read_to_string(json_path)?;
41-
let errors: Vec<SerializedError> = serde_json::from_str(&json_content)?;
42-
errors
43-
.into_iter()
44-
.filter(|e| !e.is_unused_ignore())
45-
.collect()
42+
if self.remove_unused {
43+
// Remove unused ignores mode
44+
let unused_errors: Vec<SerializedError> = if let Some(json_path) = &self.json {
45+
// Parse errors from JSON file, filtering for UnusedIgnore errors only
46+
let json_content = std::fs::read_to_string(json_path)?;
47+
let errors: Vec<SerializedError> = serde_json::from_str(&json_content)?;
48+
errors
49+
.into_iter()
50+
.filter(|e| e.is_unused_ignore())
51+
.collect()
52+
} else {
53+
// Run type checking to collect unused ignore errors
54+
self.config_override.validate()?;
55+
let (files_to_check, config_finder) =
56+
self.files.clone().resolve(self.config_override.clone())?;
57+
58+
let check_args = super::check::CheckArgs::parse_from([
59+
"check",
60+
"--output-format",
61+
"omit-errors",
62+
]);
63+
let (_, errors) = check_args.run_once(files_to_check, config_finder)?;
64+
65+
// Convert to SerializedErrors, filtering for UnusedIgnore only
66+
errors
67+
.into_iter()
68+
.filter_map(|e| SerializedError::from_error(&e))
69+
.filter(|e| e.is_unused_ignore())
70+
.collect()
71+
};
72+
73+
// Remove unused ignores
74+
suppress::remove_unused_ignores_from_serialized(unused_errors);
4675
} else {
47-
// Run type checking to collect errors
48-
self.config_override.validate()?;
49-
let (files_to_check, config_finder) =
50-
self.files.clone().resolve(self.config_override.clone())?;
76+
// Add suppressions mode (existing behavior)
77+
let serialized_errors: Vec<SerializedError> = if let Some(json_path) = &self.json {
78+
// Parse errors from JSON file, filtering out UnusedIgnore errors
79+
let json_content = std::fs::read_to_string(json_path)?;
80+
let errors: Vec<SerializedError> = serde_json::from_str(&json_content)?;
81+
errors
82+
.into_iter()
83+
.filter(|e| !e.is_unused_ignore())
84+
.collect()
85+
} else {
86+
// Run type checking to collect errors
87+
self.config_override.validate()?;
88+
let (files_to_check, config_finder) =
89+
self.files.clone().resolve(self.config_override.clone())?;
5190

52-
let check_args =
53-
super::check::CheckArgs::parse_from(["check", "--output-format", "omit-errors"]);
54-
let (_, errors) = check_args.run_once(files_to_check, config_finder)?;
91+
let check_args = super::check::CheckArgs::parse_from([
92+
"check",
93+
"--output-format",
94+
"omit-errors",
95+
]);
96+
let (_, errors) = check_args.run_once(files_to_check, config_finder)?;
5597

56-
// Convert to SerializedErrors, filtering by severity and excluding UnusedIgnore
57-
errors
58-
.into_iter()
59-
.filter(|e| e.severity() >= Severity::Warn)
60-
.filter_map(|e| SerializedError::from_error(&e))
61-
.filter(|e| !e.is_unused_ignore())
62-
.collect()
63-
};
98+
// Convert to SerializedErrors, filtering by severity and excluding UnusedIgnore
99+
errors
100+
.into_iter()
101+
.filter(|e| e.severity() >= Severity::Warn)
102+
.filter_map(|e| SerializedError::from_error(&e))
103+
.filter(|e| !e.is_unused_ignore())
104+
.collect()
105+
};
64106

65-
// Apply suppressions
66-
suppress::suppress_errors(suppressable_errors);
107+
// Apply suppressions
108+
suppress::suppress_errors(serialized_errors);
109+
}
67110

68111
Ok(CommandExitStatus::Success)
69112
}

pyrefly/lib/error/suppress.rs

Lines changed: 172 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -380,19 +380,28 @@ fn update_ignore_comment_with_used_codes(
380380
/// - "Unused `# pyrefly: ignore` comment for code(s): X, Y" -> remove entire comment
381381
/// - "Unused error code(s) in `# pyrefly: ignore`: X, Y" -> remove only those codes
382382
pub fn remove_unused_ignores(unused_ignore_errors: Vec<Error>) -> usize {
383+
let serialized: Vec<SerializedError> = unused_ignore_errors
384+
.iter()
385+
.filter_map(SerializedError::from_error)
386+
.collect();
387+
remove_unused_ignores_from_serialized(serialized)
388+
}
389+
390+
/// Removes unused ignore comments from source files using SerializedError.
391+
/// This is similar to remove_unused_ignores but works with SerializedError instead of Error,
392+
/// allowing it to be used with errors parsed from JSON.
393+
pub fn remove_unused_ignores_from_serialized(unused_ignore_errors: Vec<SerializedError>) -> usize {
383394
if unused_ignore_errors.is_empty() {
384395
return 0;
385396
}
386397

387398
// Group errors by file path
388-
let mut errors_by_path: SmallMap<PathBuf, Vec<&Error>> = SmallMap::new();
399+
let mut errors_by_path: SmallMap<PathBuf, Vec<&SerializedError>> = SmallMap::new();
389400
for error in &unused_ignore_errors {
390-
if let ModulePathDetails::FileSystem(path) = error.path().details() {
391-
errors_by_path
392-
.entry((**path).clone())
393-
.or_default()
394-
.push(error);
395-
}
401+
errors_by_path
402+
.entry(error.path.clone())
403+
.or_default()
404+
.push(error);
396405
}
397406

398407
let regex = Regex::new(r"#\s*pyrefly:\s*ignore.*$").unwrap();
@@ -401,14 +410,9 @@ pub fn remove_unused_ignores(unused_ignore_errors: Vec<Error>) -> usize {
401410

402411
for (path, path_errors) in &errors_by_path {
403412
// Build a map from line number to the error
404-
let mut line_errors: SmallMap<usize, &Error> = SmallMap::new();
413+
let mut line_errors: SmallMap<usize, &SerializedError> = SmallMap::new();
405414
for error in path_errors {
406-
let line = error
407-
.display_range()
408-
.start
409-
.line_within_file()
410-
.to_zero_indexed();
411-
line_errors.insert(line as usize, error);
415+
line_errors.insert(error.line, *error);
412416
}
413417

414418
if let Ok(file) = read_and_validate_file(path) {
@@ -421,7 +425,7 @@ pub fn remove_unused_ignores(unused_ignore_errors: Vec<Error>) -> usize {
421425
if let Some(error) = line_errors.get(&idx)
422426
&& regex.is_match(line)
423427
{
424-
let msg = error.msg();
428+
let msg = &error.message;
425429

426430
// Determine action based on error message
427431
if msg.starts_with("Unused `# pyrefly: ignore` comment") {
@@ -1000,4 +1004,157 @@ a: int = "" # pyrefly: ignore [bad-assignment]
10001004
let after = "\r\n# pyrefly: ignore [bad-assignment]\r\nx: str = 1\r\n";
10011005
assert_suppress_errors(before, after);
10021006
}
1007+
1008+
// Helper function to test remove_unused_ignores_from_serialized
1009+
fn assert_remove_ignores_from_serialized(
1010+
file_content: &str,
1011+
mut serialized_errors: Vec<SerializedError>,
1012+
expected_content: &str,
1013+
expected_removals: usize,
1014+
) {
1015+
let tdir = tempfile::tempdir().unwrap();
1016+
let path = get_path(&tdir);
1017+
fs_anyhow::write(&path, file_content).unwrap();
1018+
1019+
// Update error paths to point to the temp file
1020+
for error in &mut serialized_errors {
1021+
error.path = path.clone();
1022+
}
1023+
1024+
let removals = suppress::remove_unused_ignores_from_serialized(serialized_errors);
1025+
1026+
let got_file = fs_anyhow::read_to_string(&path).unwrap();
1027+
assert_eq!(expected_content, got_file);
1028+
assert_eq!(removals, expected_removals);
1029+
}
1030+
1031+
#[test]
1032+
fn test_remove_unused_ignores_from_serialized_blanket() {
1033+
let input = r#"def g() -> str:
1034+
return "hello" # pyrefly: ignore
1035+
"#;
1036+
let want = r#"def g() -> str:
1037+
return "hello"
1038+
"#;
1039+
let errors = vec![SerializedError {
1040+
path: PathBuf::from("test.py"),
1041+
line: 1,
1042+
name: "unused-ignore".to_owned(),
1043+
message: "Unused `# pyrefly: ignore` comment".to_owned(),
1044+
}];
1045+
assert_remove_ignores_from_serialized(input, errors, want, 1);
1046+
}
1047+
1048+
#[test]
1049+
fn test_remove_unused_ignores_from_serialized_partial() {
1050+
let before = r#"
1051+
# pyrefly: ignore[bad-assignment,bad-override]
1052+
a: int = ""
1053+
"#;
1054+
let after = r#"
1055+
# pyrefly: ignore [bad-assignment]
1056+
a: int = ""
1057+
"#;
1058+
let errors = vec![SerializedError {
1059+
path: PathBuf::from("test.py"),
1060+
line: 1,
1061+
name: "unused-ignore".to_owned(),
1062+
message: "Unused error code(s) in `# pyrefly: ignore`: bad-override".to_owned(),
1063+
}];
1064+
assert_remove_ignores_from_serialized(before, errors, after, 1);
1065+
}
1066+
1067+
#[test]
1068+
fn test_remove_unused_ignores_from_serialized_multiple_codes() {
1069+
let before = r#"
1070+
def foo() -> str:
1071+
# pyrefly: ignore [bad-return, unsupported-operation, bad-assignment]
1072+
return 1 + []
1073+
"#;
1074+
let after = r#"
1075+
def foo() -> str:
1076+
# pyrefly: ignore [bad-return, unsupported-operation]
1077+
return 1 + []
1078+
"#;
1079+
let errors = vec![SerializedError {
1080+
path: PathBuf::from("test.py"),
1081+
line: 2,
1082+
name: "unused-ignore".to_owned(),
1083+
message: "Unused error code(s) in `# pyrefly: ignore`: bad-assignment".to_owned(),
1084+
}];
1085+
assert_remove_ignores_from_serialized(before, errors, after, 1);
1086+
}
1087+
1088+
#[test]
1089+
fn test_remove_unused_ignores_from_serialized_inline() {
1090+
let input = r#"
1091+
def g() -> str:
1092+
return "hello" # pyrefly: ignore [bad-return]
1093+
"#;
1094+
let want = r#"
1095+
def g() -> str:
1096+
return "hello"
1097+
"#;
1098+
let errors = vec![SerializedError {
1099+
path: PathBuf::from("test.py"),
1100+
line: 2,
1101+
name: "unused-ignore".to_owned(),
1102+
message: "Unused `# pyrefly: ignore` comment for code(s): bad-return".to_owned(),
1103+
}];
1104+
assert_remove_ignores_from_serialized(input, errors, want, 1);
1105+
}
1106+
1107+
#[test]
1108+
fn test_remove_unused_ignores_from_serialized_multiple_files() {
1109+
let tdir = tempfile::tempdir().unwrap();
1110+
let path1 = tdir.path().join("file1.py");
1111+
let path2 = tdir.path().join("file2.py");
1112+
1113+
let content1 = "x = 1 # pyrefly: ignore\n";
1114+
let content2 = "y = 2 # pyrefly: ignore\n";
1115+
1116+
fs_anyhow::write(&path1, content1).unwrap();
1117+
fs_anyhow::write(&path2, content2).unwrap();
1118+
1119+
let errors = vec![
1120+
SerializedError {
1121+
path: path1.clone(),
1122+
line: 0,
1123+
name: "unused-ignore".to_owned(),
1124+
message: "Unused `# pyrefly: ignore` comment".to_owned(),
1125+
},
1126+
SerializedError {
1127+
path: path2.clone(),
1128+
line: 0,
1129+
name: "unused-ignore".to_owned(),
1130+
message: "Unused `# pyrefly: ignore` comment".to_owned(),
1131+
},
1132+
];
1133+
1134+
let removals = suppress::remove_unused_ignores_from_serialized(errors);
1135+
1136+
assert_eq!(fs_anyhow::read_to_string(&path1).unwrap(), "x = 1\n");
1137+
assert_eq!(fs_anyhow::read_to_string(&path2).unwrap(), "y = 2\n");
1138+
assert_eq!(removals, 2);
1139+
}
1140+
1141+
#[test]
1142+
fn test_remove_unused_ignores_from_serialized_empty_list() {
1143+
let errors: Vec<SerializedError> = vec![];
1144+
let removals = suppress::remove_unused_ignores_from_serialized(errors);
1145+
assert_eq!(removals, 0);
1146+
}
1147+
1148+
#[test]
1149+
fn test_remove_unused_ignores_from_serialized_preserves_crlf() {
1150+
let input = "def g() -> str:\r\n return \"hello\" # pyrefly: ignore [bad-return]\r\n";
1151+
let want = "def g() -> str:\r\n return \"hello\"\r\n";
1152+
let errors = vec![SerializedError {
1153+
path: PathBuf::from("test.py"),
1154+
line: 1,
1155+
name: "unused-ignore".to_owned(),
1156+
message: "Unused `# pyrefly: ignore` comment".to_owned(),
1157+
}];
1158+
assert_remove_ignores_from_serialized(input, errors, want, 1);
1159+
}
10031160
}

0 commit comments

Comments
 (0)