diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index bcd88bc272..f63e168a36 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -27,7 +27,6 @@ use ruff_python_ast::name::Name; use ruff_text_size::TextRange; use starlark_map::small_set::SmallSet; use vec1::Vec1; -use vec1::vec1; use crate::alt::answers::LookupAnswer; use crate::alt::answers_solver::AnswersSolver; @@ -614,7 +613,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } else if !error_messages.is_empty() { error_messages.sort(); error_messages.dedup(); - let mut msg = vec1![error_messages.join("\n")]; + let header = error_messages.remove(0); + let mut details = error_messages; // Skip suggestions when we have a partial union failure to avoid suggesting // attributes from the types that have them when the problem is that some types // don't have the attribute at all. @@ -623,9 +623,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .as_ref() .and_then(|attr_base| self.suggest_attribute_name(attr_name, attr_base)) { - msg.push(format!("Did you mean `{suggestion}`?")); + details.push(format!("Did you mean `{suggestion}`?")); } - let (header, details) = msg.split_off_first(); errors .error_builder(range, ErrorKind::MissingAttribute, header) .with_details(details) diff --git a/pyrefly/lib/error/error.rs b/pyrefly/lib/error/error.rs index c6292c1229..f66634a3ea 100644 --- a/pyrefly/lib/error/error.rs +++ b/pyrefly/lib/error/error.rs @@ -348,6 +348,10 @@ impl Error { error_kind: ErrorKind, ) -> Self { let display_range = module.display_range(range); + assert!( + !header.contains(['\n', '\r']), + "error header must not contain newlines" + ); let msg_header = header.into_boxed_str(); let msg_details = if details.is_empty() { None @@ -585,6 +589,23 @@ mod tests { ); } + #[test] + #[should_panic(expected = "error header must not contain newlines")] + fn test_error_header_rejects_newlines() { + let module_info = Module::new( + ModuleName::from_str("test"), + ModulePath::filesystem(PathBuf::from("test.py")), + Arc::new("x = 1".to_owned()), + ); + let _ = Error::new( + module_info, + TextRange::new(TextSize::new(0), TextSize::new(1)), + "first line\r\nsecond line".to_owned(), + Vec::new(), + ErrorKind::BadReturn, + ); + } + /// Integration test: verify that binary operator errors from the type checker /// produce secondary annotations labeling both operands with their types. #[test]