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::from_error(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::from_error(Error::Syntax {
message: "doc comments cannot be applied to parameters".to_owned(),
}),
Diagnostic::from_error(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 = "
module tests
interface TestInterface {
/// @param testParam1: A string param
/// @returns: bool
testOp(testParam1: string) -> bool
}
";
assert_parses(slice);
}
#[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::from_lint(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::from_lint(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")]
#[test_case("Foo::op::a", "parameters"; "parameter")]
fn doc_comment_links_to_invalid_element(link_identifier: &str, kind: &str) {
let slice = format!(
"
module tests
interface Foo {{
op(a: string)
}}
/// A test struct, should probably use {{@link {link_identifier}}}.
struct TestStruct {{}}
"
);
let diagnostics = parse_for_diagnostics(slice);
let expected = Diagnostic::from_lint(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::from_lint(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::from_lint(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::from_lint(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::from_lint(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::from_lint(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 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::from_lint(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::from_lint(Lint::IncorrectDocComment {
message: "comment has a 'returns' tag, but only operations can return".to_owned(),
});
check_diagnostics(diagnostics, [expected]);
}
}