ruff/crates/ty_test/src/matcher.rs

1320 lines
39 KiB
Rust

//! Match [`Diagnostic`]s against assertions and produce test failure
//! messages for any mismatches.
use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use colored::Colorize;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File;
use ruff_db::source::{SourceText, line_index, source_text};
use ruff_source_file::{LineIndex, OneIndexed};
use std::cmp::Ordering;
use std::ops::Range;
#[derive(Debug, Default)]
pub(super) struct FailuresByLine {
failures: Vec<String>,
lines: Vec<LineFailures>,
}
impl FailuresByLine {
pub(super) fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[String])> {
self.lines.iter().map(|line_failures| {
(
line_failures.line_number,
&self.failures[line_failures.range.clone()],
)
})
}
pub(super) fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
let start = self.failures.len();
self.failures.extend(messages);
self.lines.push(LineFailures {
line_number,
range: start..self.failures.len(),
});
}
fn is_empty(&self) -> bool {
self.lines.is_empty()
}
}
#[derive(Debug)]
struct LineFailures {
line_number: OneIndexed,
range: Range<usize>,
}
pub(super) fn match_file(
db: &Db,
file: File,
diagnostics: &[Diagnostic],
) -> Result<(), FailuresByLine> {
// Parse assertions from comments in the file, and get diagnostics from the file; both
// ordered by line number.
let assertions = InlineFileAssertions::from_file(db, file);
let diagnostics = SortedDiagnostics::new(diagnostics, &line_index(db, file));
// Get iterators over assertions and diagnostics grouped by line, in ascending line order.
let mut line_assertions = assertions.into_iter();
let mut line_diagnostics = diagnostics.iter_lines();
let mut current_assertions = line_assertions.next();
let mut current_diagnostics = line_diagnostics.next();
let matcher = Matcher::from_file(db, file);
let mut failures = FailuresByLine::default();
loop {
match (&current_assertions, &current_diagnostics) {
(Some(assertions), Some(diagnostics)) => {
match assertions.line_number.cmp(&diagnostics.line_number) {
Ordering::Equal => {
// We have assertions and diagnostics on the same line; check for
// matches and error on any that don't match, then advance both
// iterators.
matcher
.match_line(diagnostics, assertions)
.unwrap_or_else(|messages| {
failures.push(assertions.line_number, messages);
});
current_assertions = line_assertions.next();
current_diagnostics = line_diagnostics.next();
}
Ordering::Less => {
// We have assertions on an earlier line than diagnostics; report these
// assertions as all unmatched, and advance the assertions iterator.
failures.push(assertions.line_number, unmatched(assertions));
current_assertions = line_assertions.next();
}
Ordering::Greater => {
// We have diagnostics on an earlier line than assertions; report these
// diagnostics as all unmatched, and advance the diagnostics iterator.
failures.push(diagnostics.line_number, unmatched(diagnostics));
current_diagnostics = line_diagnostics.next();
}
}
}
(Some(assertions), None) => {
// We've exhausted diagnostics but still have assertions; report these assertions
// as unmatched and advance the assertions iterator.
failures.push(assertions.line_number, unmatched(assertions));
current_assertions = line_assertions.next();
}
(None, Some(diagnostics)) => {
// We've exhausted assertions but still have diagnostics; report these
// diagnostics as unmatched and advance the diagnostics iterator.
failures.push(diagnostics.line_number, unmatched(diagnostics));
current_diagnostics = line_diagnostics.next();
}
// When we've exhausted both diagnostics and assertions, break.
(None, None) => break,
}
}
if failures.is_empty() {
Ok(())
} else {
Err(failures)
}
}
trait Unmatched {
fn unmatched(&self) -> String;
}
fn unmatched<'a, T: Unmatched + 'a>(unmatched: &'a [T]) -> Vec<String> {
unmatched.iter().map(Unmatched::unmatched).collect()
}
trait UnmatchedWithColumn {
fn unmatched_with_column(&self, column: OneIndexed) -> String;
}
// This is necessary since we only parse assertions lazily,
// and sometimes we know before parsing any assertions that an assertion will be unmatched,
// e.g. if we've exhausted all diagnostics but there are still assertions left.
//
// TODO: the lazy parsing means that we sometimes won't report malformed assertions as
// being invalid if we detect that they'll be unmatched before parsing them.
// That's perhaps not the best user experience.
impl Unmatched for UnparsedAssertion<'_> {
fn unmatched(&self) -> String {
format!("{} {self}", "unmatched assertion:".red())
}
}
impl Unmatched for ParsedAssertion<'_> {
fn unmatched(&self) -> String {
format!("{} {self}", "unmatched assertion:".red())
}
}
fn maybe_add_undefined_reveal_clarification(
diagnostic: &Diagnostic,
original: std::fmt::Arguments,
) -> String {
if diagnostic.id().is_lint_named("undefined-reveal") {
format!(
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
"used built-in `reveal_type`:".yellow()
)
} else {
format!("{} {original}", "unexpected error:".red())
}
}
impl Unmatched for &Diagnostic {
fn unmatched(&self) -> String {
maybe_add_undefined_reveal_clarification(
self,
format_args!(
r#"[{id}] "{message}""#,
id = self.id(),
message = self.concise_message()
),
)
}
}
impl UnmatchedWithColumn for &Diagnostic {
fn unmatched_with_column(&self, column: OneIndexed) -> String {
maybe_add_undefined_reveal_clarification(
self,
format_args!(
r#"{column} [{id}] "{message}""#,
id = self.id(),
message = self.concise_message()
),
)
}
}
/// Discard `@Todo`-type metadata from expected types, which is not available
/// when running in release mode.
#[cfg(not(debug_assertions))]
fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> {
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
TODO_METADATA_REGEX.replace_all(ty, "@Todo")
}
struct Matcher {
line_index: LineIndex,
source: SourceText,
}
impl Matcher {
fn from_file(db: &Db, file: File) -> Self {
Self {
line_index: line_index(db, file),
source: source_text(db, file),
}
}
/// Check a slice of [`Diagnostic`]s against a slice of
/// [`UnparsedAssertion`]s.
///
/// Return vector of [`Unmatched`] for any unmatched diagnostics or
/// assertions.
fn match_line<'a, 'b>(
&self,
diagnostics: &'a [&'a Diagnostic],
assertions: &'a [UnparsedAssertion<'b>],
) -> Result<(), Vec<String>>
where
'b: 'a,
{
let mut failures = vec![];
let mut unmatched = diagnostics.to_vec();
for assertion in assertions {
match assertion.parse() {
Ok(assertion) => {
if !self.matches(&assertion, &mut unmatched) {
failures.push(assertion.unmatched());
}
}
Err(error) => {
failures.push(format!("{} {}", "invalid assertion:".red(), error));
}
}
}
for diagnostic in unmatched {
failures.push(diagnostic.unmatched_with_column(self.column(diagnostic)));
}
if failures.is_empty() {
Ok(())
} else {
Err(failures)
}
}
fn column(&self, diagnostic: &Diagnostic) -> OneIndexed {
diagnostic
.primary_span()
.and_then(|span| span.range())
.map(|range| {
self.line_index
.line_column(range.start(), &self.source)
.column
})
.unwrap_or(OneIndexed::from_zero_indexed(0))
}
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
///
/// If so, return `true` and remove the matched diagnostics from `unmatched`. Otherwise, return
/// `false`.
///
/// An `Error` assertion can only match one diagnostic; even if it could match more than one,
/// we short-circuit after the first match.
///
/// A `Revealed` assertion must match a revealed-type diagnostic, and may also match an
/// undefined-reveal diagnostic, if present.
fn matches(&self, assertion: &ParsedAssertion, unmatched: &mut Vec<&Diagnostic>) -> bool {
match assertion {
ParsedAssertion::Error(error) => {
let position = unmatched.iter().position(|diagnostic| {
let lint_name_matches = !error.rule.is_some_and(|rule| {
!(diagnostic.id().is_lint_named(rule) || diagnostic.id().as_str() == rule)
});
let column_matches = error
.column
.is_none_or(|col| col == self.column(diagnostic));
let message_matches = error.message_contains.is_none_or(|needle| {
diagnostic.concise_message().to_string().contains(needle)
});
lint_name_matches && column_matches && message_matches
});
if let Some(position) = position {
unmatched.swap_remove(position);
true
} else {
false
}
}
ParsedAssertion::Revealed(expected_type) => {
#[cfg(not(debug_assertions))]
let expected_type = discard_todo_metadata(&expected_type);
let mut matched_revealed_type = None;
let mut matched_undefined_reveal = None;
let expected_reveal_type_message = format!("`{expected_type}`");
for (index, diagnostic) in unmatched.iter().enumerate() {
if matched_revealed_type.is_none()
&& diagnostic.id() == DiagnosticId::RevealedType
&& diagnostic
.primary_annotation()
.and_then(|a| a.get_message())
.unwrap_or_default()
== expected_reveal_type_message
{
matched_revealed_type = Some(index);
} else if matched_undefined_reveal.is_none()
&& diagnostic.id().is_lint_named("undefined-reveal")
{
matched_undefined_reveal = Some(index);
}
if matched_revealed_type.is_some() && matched_undefined_reveal.is_some() {
break;
}
}
let mut idx = 0;
unmatched.retain(|_| {
let retain =
Some(idx) != matched_revealed_type && Some(idx) != matched_undefined_reveal;
idx += 1;
retain
});
matched_revealed_type.is_some()
}
}
}
}
#[cfg(test)]
mod tests {
use super::FailuresByLine;
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed;
use ruff_text_size::TextRange;
use ty_python_semantic::{
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
};
struct ExpectedDiagnostic {
id: DiagnosticId,
message: &'static str,
range: TextRange,
}
impl ExpectedDiagnostic {
fn new(id: DiagnosticId, message: &'static str, offset: usize) -> Self {
let offset: u32 = offset.try_into().unwrap();
Self {
id,
message,
range: TextRange::new(offset.into(), (offset + 1).into()),
}
}
fn into_diagnostic(self, file: File) -> Diagnostic {
let mut diag = if self.id == DiagnosticId::RevealedType {
Diagnostic::new(self.id, Severity::Error, "Revealed type")
} else {
Diagnostic::new(self.id, Severity::Error, "")
};
let span = Span::from(file).with_range(self.range);
diag.annotate(Annotation::primary(span).message(self.message));
diag
}
}
fn get_result(
source: &str,
expected_diagnostics: Vec<ExpectedDiagnostic>,
) -> Result<(), FailuresByLine> {
colored::control::set_override(false);
let mut db = crate::db::Db::setup();
let settings = ProgramSettings {
python_version: Some(PythonVersionWithSource::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(Vec::new()),
};
match Program::try_get(&db) {
Some(program) => program.update_from_settings(&mut db, settings),
None => Program::from_settings(&db, settings).map(|_| ()),
}
.expect("Failed to update Program settings in TestDb");
db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap();
let diagnostics: Vec<Diagnostic> = expected_diagnostics
.into_iter()
.map(|diagnostic| diagnostic.into_diagnostic(file))
.collect();
super::match_file(&db, file, &diagnostics)
}
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
let Err(failures) = result else {
panic!("expected a failure");
};
let expected: Vec<(OneIndexed, Vec<String>)> = messages
.iter()
.map(|(idx, msgs)| {
(
OneIndexed::from_zero_indexed(*idx),
msgs.iter().map(ToString::to_string).collect(),
)
})
.collect();
let failures: Vec<(OneIndexed, Vec<String>)> = failures
.iter()
.map(|(idx, msgs)| (idx, msgs.to_vec()))
.collect();
assert_eq!(failures, expected);
}
fn assert_ok(result: &Result<(), FailuresByLine>) {
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn revealed_match() {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"`Foo`",
0,
)],
);
assert_ok(&result);
}
#[test]
fn revealed_wrong_rule() {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("not-revealed-type"),
"`Foo`",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [not-revealed-type] "`Foo`""#,
],
)],
);
}
#[test]
fn revealed_wrong_message() {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Something else",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [revealed-type] "Revealed type: Something else""#,
],
)],
);
}
#[test]
fn revealed_unmatched() {
let result = get_result("x # revealed: Foo", vec![]);
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
}
#[test]
fn revealed_match_with_undefined() {
let result = get_result(
"x # revealed: Foo",
vec![
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "`Foo`", 0),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
],
);
assert_ok(&result);
}
#[test]
fn revealed_match_with_only_undefined() {
let result = get_result(
"x # revealed: Foo",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
)],
);
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
}
#[test]
fn revealed_mismatch_with_undefined() {
let result = get_result(
"x # revealed: Foo",
vec![
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "`Bar`", 0),
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [revealed-type] "Revealed type: `Bar`""#,
],
)],
);
}
#[test]
fn undefined_reveal_type_unmatched() {
let result = get_result(
"reveal_type(1)",
vec![
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "`Literal[1]`", 12),
],
);
assert_fail(
result,
&[(
0,
&[
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: [revealed-type] "Revealed type: `Literal[1]`""#,
],
)],
);
}
#[test]
fn undefined_reveal_type_mismatched() {
let result = get_result(
"reveal_type(1) # error: [something-else]",
vec![
ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "`Literal[1]`", 12),
],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: error: [something-else]",
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: 13 [revealed-type] "Revealed type: `Literal[1]`""#,
],
)],
);
}
#[test]
fn error_unmatched() {
let result = get_result("x # error: [rule]", vec![]);
assert_fail(result, &[(0, &["unmatched assertion: error: [rule]"])]);
}
#[test]
fn error_match_rule() {
let result = get_result(
"x # error: [some-rule]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_rule_no_whitespace() {
let result = get_result(
"x #error:[some-rule]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_rule_lots_of_whitespace() {
let result = get_result(
"x # error : [ some-rule ]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_wrong_rule() {
let result = get_result(
"x # error: [some-rule]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: error: [some-rule]",
r#"unexpected error: 1 [anything] "Any message""#,
],
)],
);
}
#[test]
fn error_match_message() {
let result = get_result(
r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"message contains this",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_wrong_message() {
let result = get_result(
r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
r#"unmatched assertion: error: "contains this""#,
r#"unexpected error: 1 [anything] "Any message""#,
],
)],
);
}
#[test]
fn error_match_column_and_rule() {
let result = get_result(
"x # error: 1 [some-rule]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_column_and_rule_and_message() {
let result = get_result(
r#"x # error: 5 [some-rule] "Some message""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Some message",
4,
)],
);
assert_ok(&result);
}
#[test]
fn error_wrong_column() {
let result = get_result(
"x # error: 2 [rule]",
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("rule"),
"Any message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"unmatched assertion: error: 2 [rule]",
r#"unexpected error: 1 [rule] "Any message""#,
],
)],
);
}
#[test]
fn error_match_column_and_message() {
let result = get_result(
r#"x # error: 1 "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"message contains this",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_rule_and_message() {
let result = get_result(
r#"x # error: [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("a-rule"),
"message contains this",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_all() {
let result = get_result(
r#"x # error: 1 [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("a-rule"),
"message contains this",
0,
)],
);
assert_ok(&result);
}
#[test]
fn error_match_all_wrong_column() {
let result = get_result(
r#"x # error: 2 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"message contains this",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "message contains this""#,
],
)],
);
}
#[test]
fn error_match_all_wrong_rule() {
let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("other-rule"),
"message contains this",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [other-rule] "message contains this""#,
],
)],
);
}
#[test]
fn error_match_all_wrong_message() {
let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "Any message""#,
],
)],
);
}
#[test]
fn interspersed_matches_and_mismatches() {
let source = dedent(
r#"
1 # error: [line-one]
2
3 # error: [line-three]
4 # error: [line-four]
5
6: # error: [line-six]
"#,
);
let two = source.find('2').unwrap();
let three = source.find('3').unwrap();
let five = source.find('5').unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
ExpectedDiagnostic::new(DiagnosticId::lint("line-three"), "msg", three),
ExpectedDiagnostic::new(DiagnosticId::lint("line-five"), "msg", five),
],
);
assert_fail(
result,
&[
(1, &["unmatched assertion: error: [line-one]"]),
(2, &[r#"unexpected error: [line-two] "msg""#]),
(4, &["unmatched assertion: error: [line-four]"]),
(5, &[r#"unexpected error: [line-five] "msg""#]),
(6, &["unmatched assertion: error: [line-six]"]),
],
);
}
#[test]
fn more_diagnostics_than_assertions() {
let source = dedent(
r#"
1 # error: [line-one]
2
"#,
);
let one = source.find('1').unwrap();
let two = source.find('2').unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("line-one"), "msg", one),
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
],
);
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
}
#[test]
fn multiple_assertions_and_diagnostics_same_line() {
let source = dedent(
"
# error: [one-rule]
# error: [other-rule]
x
",
);
let x = source.find('x').unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
],
);
assert_ok(&result);
}
#[test]
fn multiple_assertions_and_diagnostics_same_line_all_same() {
let source = dedent(
"
# error: [one-rule]
# error: [one-rule]
x
",
);
let x = source.find('x').unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
],
);
assert_ok(&result);
}
#[test]
fn multiple_assertions_and_diagnostics_same_line_mismatch() {
let source = dedent(
"
# error: [one-rule]
# error: [other-rule]
x
",
);
let x = source.find('x').unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
ExpectedDiagnostic::new(DiagnosticId::lint("third-rule"), "msg", x),
],
);
assert_fail(
result,
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
);
}
#[test]
fn parenthesized_expression() {
let source = dedent(
"
a = b + (
error: [undefined-reveal]
reveal_type(5) # revealed: Literal[5]
)
",
);
let reveal = source.find("reveal_type").unwrap();
let result = get_result(
&source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("undefined-reveal"), "msg", reveal),
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "`Literal[5]`", reveal),
],
);
assert_ok(&result);
}
#[test]
fn bare_error_assertion_not_allowed() {
let source = "x # error:";
let x = source.find('x').unwrap();
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn bare_reveal_assertion_not_allowed() {
let source = "x # revealed: ";
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: Must specify which type should be revealed",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn column_only_error_assertion_not_allowed() {
let source = "x # error: 1";
let x = source.find('x').unwrap();
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn unclosed_rule_not_allowed() {
let source = r#"x # error: 42 [some-rule "Some message""#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: expected ']' to close rule code",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn bad_column_number_not_allowed() {
let source = r#"x # error: 3.14 [some-rule] "Some message""#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: bad column number `3.14`",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn multiple_column_numbers_not_allowed() {
let source = r#"x # error: 3 14 [some-rule] "Some message""#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: multiple column numbers in one assertion",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn multiple_column_numbers_not_allowed_even_if_interspersed() {
let source = r#"x # error: 3 [some-rule] 14 "Some message""#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: multiple column numbers in one assertion",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn two_rule_codes_not_allowed() {
let source = r#"x # error: [rule1] [rule2] "Some message""#;
let result = get_result(
source,
vec![
ExpectedDiagnostic::new(DiagnosticId::lint("rule1"), "Some message", 0),
ExpectedDiagnostic::new(DiagnosticId::lint("rule2"), "Some message", 0),
],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: cannot use multiple rule codes in one assertion",
r#"unexpected error: 1 [rule1] "Some message""#,
r#"unexpected error: 1 [rule2] "Some message""#,
],
)],
);
}
#[test]
fn column_number_not_allowed_after_rule_code() {
let source = r#"x # error: [rule1] 4 "Some message""#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("rule1"),
"Some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: column number must precede the rule code",
r#"unexpected error: 1 [rule1] "Some message""#,
],
)],
);
}
#[test]
fn column_number_not_allowed_after_message() {
let source = r#"x # error: "Some message" 0"#;
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("rule1"),
"Some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: expected '\"' to be the final character in an assertion with an error message",
r#"unexpected error: 1 [rule1] "Some message""#,
],
)],
);
}
#[test]
fn unclosed_message_not_allowed() {
let source = "x # error: \"Some message";
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: expected '\"' to be the final character in an assertion with an error message",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
#[test]
fn unclosed_message_not_allowed_even_after_rule_code() {
let source = "x # error: [some-rule] \"Some message";
let result = get_result(
source,
vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
0,
)],
);
assert_fail(
result,
&[(
0,
&[
"invalid assertion: expected '\"' to be the final character in an assertion with an error message",
r#"unexpected error: 1 [some-rule] "some message""#,
],
)],
);
}
}