mod test_helpers;
mod comments {
use crate::test_helpers::*;
use slicec::diagnostics::{Diagnostic, Error, Lint};
use slicec::grammar::*;
use test_case::test_case;
#[test]
fn single_line_doc_comment() {
let slice = "
module tests
/// This is a single line doc comment.
interface MyInterface {}
";
let ast = parse_for_ast(slice);
let interface_def = ast.find_element::<Interface>("tests::MyInterface").unwrap();
let interface_doc = interface_def.comment().unwrap();
assert_eq!(interface_doc.span.start, (4, 13).into());
assert_eq!(interface_doc.span.end, (4, 51).into());
let overview = &interface_doc.overview.as_ref().unwrap();
assert_eq!(overview.span.start, (4, 16).into());
assert_eq!(overview.span.end, (4, 51).into());
let message = &overview.value;
assert_eq!(message.len(), 2);
let MessageComponent::Text(text) = &message[0] else { panic!() };
assert_eq!(text, "This is a single line doc comment.");
let MessageComponent::Text(newline) = &message[1] else { panic!() };
assert_eq!(newline, "\n");
}
#[test]
fn multi_line_doc_comment() {
let slice = "
module tests
/// This is a
/// multiline doc comment.
interface MyInterface {}
";
let ast = parse_for_ast(slice);
let interface_def = ast.find_element::<Interface>("tests::MyInterface").unwrap();
let interface_doc = interface_def.comment().unwrap();
assert_eq!(interface_doc.span.start, (4, 13).into());
assert_eq!(interface_doc.span.end, (5, 39).into());
let overview = &interface_doc.overview.as_ref().unwrap();
assert_eq!(overview.span.start, (4, 16).into());
assert_eq!(overview.span.end, (5, 39).into());
let message = &overview.value;
assert_eq!(message.len(), 4);
let MessageComponent::Text(text) = &message[0] else { panic!() };
assert_eq!(text, "This is a");
let MessageComponent::Text(newline) = &message[1] else { panic!() };
assert_eq!(newline, "\n");
let MessageComponent::Text(text) = &message[2] else { panic!() };
assert_eq!(text, "multiline doc comment.");
let MessageComponent::Text(newline) = &message[3] else { panic!() };
assert_eq!(newline, "\n");
}
#[test]
fn doc_comments_params() {
let slice = "
module tests
interface TestInterface {
/// @param testParam: My test param
testOp(testParam: string)
}
";
let ast = parse_for_ast(slice);
let operation = ast.find_element::<Operation>("tests::TestInterface::testOp").unwrap();
let param_tags = &operation.comment().unwrap().params;
assert_eq!(param_tags.len(), 1);
let param_tag = ¶m_tags[0];
assert_eq!(param_tag.span.start, (5, 21).into());
assert_eq!(param_tag.span.end, (5, 37).into());
let identifier = ¶m_tag.identifier;
assert_eq!(identifier.value, "testParam");
assert_eq!(identifier.span.start, (5, 28).into());
assert_eq!(identifier.span.end, (5, 37).into());
let message = ¶m_tag.message;
assert_eq!(message.span.start, (5, 37).into());
assert_eq!(message.span.end, (5, 52).into());
let components = &message.value;
assert_eq!(components.len(), 2);
let MessageComponent::Text(text) = &components[0] else { panic!() };
assert_eq!(text, "My test param");
}
#[test]
fn doc_comments_returns() {
let slice = "
module tests
interface TestInterface {
/// @returns bool
testOp(testParam: string) -> bool
}
";
let ast = parse_for_ast(slice);
let operation = ast.find_element::<Operation>("tests::TestInterface::testOp").unwrap();
let returns_tags = &operation.comment().unwrap().returns;
assert_eq!(returns_tags.len(), 1);
let returns_tag = &returns_tags[0];
assert_eq!(returns_tag.span.start, (5, 21).into());
assert_eq!(returns_tag.span.end, (5, 34).into());
let identifier = &returns_tag.identifier.as_ref().unwrap();
assert_eq!(identifier.value, "bool");
assert_eq!(identifier.span.start, (5, 30).into());
assert_eq!(identifier.span.end, (5, 34).into());
let message = &returns_tag.message;
assert!(message.value.is_empty());
assert_eq!(message.span.start, (5, 34).into());
assert_eq!(message.span.end, (5, 34).into());
}
#[test]
fn doc_comments_not_supported_on_modules() {
let slice = "
/// This is a module comment.
module tests
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Error::Syntax {
message: "doc comments cannot be applied to modules".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn doc_comment_not_supported_on_params_and_returns() {
let slice = "
module tests
interface I {
testOp(
/// comment on param
testParam: string,
)
testOpTwo() -> (
/// comment on return
foo: string,
bar: string,
)
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = [
Diagnostic::new(Error::Syntax {
message: "doc comments cannot be applied to parameters".to_owned(),
}),
Diagnostic::new(Error::Syntax {
message: "doc comments cannot be applied to parameters".to_owned(),
}),
];
check_diagnostics(diagnostics, expected);
}
#[test]
fn operation_with_correct_doc_comments() {
let slice = "
mode = Slice1
module tests
exception MyException {}
interface TestInterface {
/// @param testParam1: A string param
/// @returns: bool
/// @throws MyException: Some message about why testOp throws
testOp(testParam1: string) -> bool throws MyException
}
";
assert_parses(slice);
}
#[test]
fn doc_comment_throws() {
let slice = "
mode = Slice1
module tests
exception MyException {}
interface TestInterface {
/// @throws MyException: Message about my thrown thing.
testOp(testParam: string) -> bool throws MyException
}
";
let ast = parse_for_ast(slice);
let operation = ast.find_element::<Operation>("tests::TestInterface::testOp").unwrap();
let throws_tags = &operation.comment().unwrap().throws;
assert_eq!(throws_tags.len(), 1);
let throws_tag = &throws_tags[0];
assert_eq!(throws_tag.span.start, (8, 21).into());
assert_eq!(throws_tag.span.end, (8, 40).into());
let thrown_type = throws_tag.thrown_type().unwrap();
assert_eq!(thrown_type.parser_scoped_identifier(), "tests::MyException");
let message = &throws_tag.message;
assert_eq!(message.span.start, (8, 40).into());
assert_eq!(message.span.end, (8, 72).into());
let components = &message.value;
assert_eq!(components.len(), 2);
let MessageComponent::Text(text) = &components[0] else { panic!() };
assert_eq!(text, "Message about my thrown thing.");
}
#[test]
fn doc_comments_see() {
let slice = "
module tests
interface TestInterface {
/// @see MySee
testOp(testParam: string) -> bool
}
";
let ast = parse_for_ast(slice);
let operation = ast.find_element::<Operation>("tests::TestInterface::testOp").unwrap();
let see_tags = &operation.comment().unwrap().see;
assert_eq!(see_tags.len(), 1);
let see_tag = &see_tags[0];
assert_eq!(see_tag.span.start, (5, 21).into());
assert_eq!(see_tag.span.end, (5, 31).into());
let Err(link_identifier) = see_tag.linked_entity() else { panic!() };
assert_eq!(link_identifier.value, "MySee");
assert_eq!(link_identifier.span.start, (5, 26).into());
assert_eq!(link_identifier.span.end, (5, 31).into());
}
#[test_case("/* This is a block comment. */"; "block comment")]
#[test_case("/*\n* This is a multiline block comment.\n */"; "multi-line block comment")]
#[test_case("// This is a comment."; "comment")]
fn non_doc_comments_are_ignored(comment: &str) {
let slice = format!(
"
module tests
{comment}
interface MyInterface {{}}
"
);
let ast = parse_for_ast(slice);
let interface_def = ast.find_element::<Interface>("tests::MyInterface").unwrap();
let interface_doc = interface_def.comment();
assert!(interface_doc.is_none());
}
#[test]
fn doc_comments_must_start_with_exactly_3_slashes() {
let slice = "
module Test
//// This is not a doc comment.
struct Foo {}
";
let ast = parse_for_ast(slice);
let struct_def = ast.find_element::<Struct>("Test::Foo").unwrap();
let struct_doc = struct_def.comment();
assert!(struct_doc.is_none());
}
#[test]
fn doc_comment_linked_identifiers() {
let slice = "
module tests
/// This comment is for {@link TestStruct}
struct TestStruct {}
";
let ast = parse_for_ast(slice);
let struct_def = ast.find_element::<Struct>("tests::TestStruct").unwrap();
let overview = &struct_def.comment().unwrap().overview;
let message = &overview.as_ref().unwrap().value;
assert_eq!(message.len(), 3);
let MessageComponent::Text(text) = &message[0] else { panic!() };
assert_eq!(text, "This comment is for ");
let MessageComponent::Link(link) = &message[1] else { panic!() };
assert_eq!(link.linked_entity().unwrap().identifier(), "TestStruct");
let MessageComponent::Text(newline) = &message[2] else { panic!() };
assert_eq!(newline, "\n");
}
#[test]
fn unknown_doc_comment_tag() {
let slice = "
module tests
/// A test struct. Similar to {@linked OtherStruct}{}.
struct TestStruct {}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::MalformedDocComment {
message: "unknown doc comment tag 'linked'".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn missing_doc_comment_linked_identifiers() {
let slice = "
module tests
/// A test struct. Similar to {@link OtherStruct}.
struct TestStruct {}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::BrokenDocLink {
message: "no element named 'OtherStruct' exists in scope".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test_case("bool", "primitive types"; "primitive")]
#[test_case("tests", "modules"; "module")]
fn doc_comment_links_to_invalid_element(link_identifier: &str, kind: &str) {
let slice = format!(
"
module tests
/// A test struct, should probably use {{@link {link_identifier}}}.
struct TestStruct {{}}
"
);
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::BrokenDocLink {
message: format!("{kind} cannot be linked to"),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn param_tag_is_rejected_for_operations_with_no_parameters() {
let slice = "
module tests
interface I {
/// @param foo: this parameter doesn't exist.
op()
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'param' tag for 'foo', but operation 'op' has no parameter with that name"
.to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn param_tag_is_rejected_if_its_identifier_does_not_match_a_parameters() {
let slice = "
module tests
interface I {
/// @param foo: this parameter doesn't exist.
op(bar: bool)
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'param' tag for 'foo', but operation 'op' has no parameter with that name"
.to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test_case("returns"; "unnamed tag")]
#[test_case("returns foo"; "named tag")]
fn returns_tag_is_rejected_for_operations_that_return_nothing(returns_tag: &str) {
let slice = format!(
"
module tests
interface I {{
/// @{returns_tag}: this tag is invalid.
op()
}}
",
);
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'returns' tag, but operation 'op' does not return anything".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn named_returns_tag_is_rejected_for_operations_that_return_an_unnamed_type() {
let slice = "
module tests
interface I {
/// @returns foo: this tag is invalid.
op() -> bool
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'returns' tag for 'foo', but operation 'op' doesn't return anything with that name"
.to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn named_returns_tag_is_rejected_if_its_identifier_does_not_match_a_return_tuple_elements() {
let slice = "
module tests
interface I {
/// @returns foo: this tag is invalid.
op() -> (alice: bool, bob: bool)
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'returns' tag for 'foo', but operation 'op' doesn't return anything with that name"
.to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn throws_tag_can_specify_scoped_exception() {
let slice1 = "
mode = Slice1
module Tests
exception E {}
";
let slice2 = "
mode = Slice1
module Other
interface Foo {
/// @throws Tests::E: it failed
op() throws Tests::E
}
";
let ast = parse_multiple_for_ast(&[slice1, slice2]);
let operation = ast.find_element::<Operation>("Other::Foo::op").unwrap();
let thrown_type = operation.exception_specification[0].definition();
let doc_comment = operation.comment().unwrap();
let TypeRefDefinition::Patched(documented_exception) = &doc_comment.throws[0].thrown_type else { panic!() };
assert_eq!(documented_exception, &thrown_type);
}
#[test]
fn throws_tag_is_rejected_for_operations_that_do_not_throw() {
let slice = format!(
"
mode = Slice1
module tests
exception Foo {{}}
interface I {{
/// @throws Foo: this tag is invalid.
op()
}}
",
);
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag, but operation 'op' does not throw anything".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn throws_tag_is_rejected_if_it_names_an_unthrowable_type() {
let slice = "
mode = Slice1
module tests
interface I {
/// @throws I: this type isn't an exception.
op()
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = [
Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag for 'I', but it is not a throwable type".to_owned(),
}),
Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag, but operation 'op' does not throw anything".to_owned(),
}),
];
check_diagnostics(diagnostics, expected);
}
#[test_case("E1"; "single")]
#[test_case("(E1, E2)"; "multiple")]
fn named_throws_tag_is_rejected_if_its_types_does_not_match_the_thrown_type(exceptions: &str) {
let slice = format!(
"
mode = Slice1
module tests
exception E1 {{}}
exception E2 {{}}
exception Foo {{}}
interface I {{
/// @throws Foo: this isn't the type that is thrown.
op() throws {exceptions}
}}
"
);
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag for 'Foo', but operation 'op' doesn't throw this exception".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn multiple_throws_tags_can_be_specified() {
let slice = "
mode = Slice1
module tests
exception E1 {}
exception E2 {}
interface I {
/// @throws E1: first exception.
/// @throws E2: second exception.
op() throws (E1, E2)
}
";
assert_parses(slice);
}
#[test]
fn throws_tag_can_be_derived_exception() {
let slice = "
mode = Slice1
module Tests
exception Base {}
exception Middle1: Base {}
exception Middle2: Base {}
exception Derived: Middle1 {}
interface I {
/// @throws Middle1
/// @throws Derived
op() throws Middle1
}
";
assert_parses(slice);
}
#[test]
fn throws_tag_cannot_be_base_exception() {
let slice = "
mode = Slice1
module Tests
exception Base {}
exception Middle1: Base {}
exception Middle2: Base {}
exception Derived: Middle1 {}
interface I {
/// @throws Base
/// @throws Middle2
op() throws Middle1
}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = [
Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag for 'Base', but operation 'op' doesn't throw this exception"
.to_owned(),
}),
Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag for 'Middle2', but operation 'op' doesn't throw this exception"
.to_owned(),
}),
];
check_diagnostics(diagnostics, expected);
}
#[test]
fn param_tags_can_only_be_used_with_operations() {
let slice = "
module tests
/// @param foo: bad tag.
struct Foo {}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'param' tag, but only operations can have parameters".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn returns_tags_can_only_be_used_with_operations() {
let slice = "
module tests
/// @returns: bad tag.
struct Foo {}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'returns' tag, but only operations can return".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
#[test]
fn throws_tags_can_only_be_used_with_operations() {
let slice = "
mode = Slice1
module tests
exception E {}
/// @throws E: bad tag.
interface Foo {}
";
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::new(Lint::IncorrectDocComment {
message: "comment has a 'throws' tag, but only operations can throw".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
}