// Copyright (c) ZeroC, Inc. 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() { // Arrange let slice = " module tests /// This is a single line doc comment. interface MyInterface {} "; // Act let ast = parse_for_ast(slice); // Assert let interface_def = ast.find_element::("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() { // Arrange let slice = " module tests /// This is a /// multiline doc comment. interface MyInterface {} "; // Act let ast = parse_for_ast(slice); // Assert let interface_def = ast.find_element::("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() { // Arrange let slice = " module tests interface TestInterface { /// @param testParam: My test param testOp(testParam: string) } "; // Act let ast = parse_for_ast(slice); // Assert let operation = ast.find_element::("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() { // Arrange let slice = " module tests interface TestInterface { /// @returns bool testOp(testParam: string) -> bool } "; // Act let ast = parse_for_ast(slice); // Assert let operation = ast.find_element::("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() { // Arrange let slice = " /// This is a module comment. module tests "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests interface I { testOp( /// comment on param testParam: string, ) testOpTwo() -> ( /// comment on return foo: string, bar: string, ) } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert let expected = [ Diagnostic::new(Error::Syntax { message: "doc comments cannot be applied to parameters".to_owned(), }), Diagnostic::new(Error::Syntax { // TODO: improve the message for return members, since they're not parameters. // We need to find an umbrella term for return members and parameters. message: "doc comments cannot be applied to parameters".to_owned(), }), ]; check_diagnostics(diagnostics, expected); } #[test] fn operation_with_correct_doc_comments() { // Arrange 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 } "; // Act/Assert assert_parses(slice); } #[test] fn doc_comment_throws() { // Arrange let slice = " mode = Slice1 module tests exception MyException {} interface TestInterface { /// @throws MyException: Message about my thrown thing. testOp(testParam: string) -> bool throws MyException } "; // Act let ast = parse_for_ast(slice); // Assert let operation = ast.find_element::("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() { // Arrange let slice = " module tests interface TestInterface { /// @see MySee testOp(testParam: string) -> bool } "; // Act let ast = parse_for_ast(slice); // Assert let operation = ast.find_element::("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) { // Arrange let slice = format!( " module tests {comment} interface MyInterface {{}} " ); // Act let ast = parse_for_ast(slice); // Assert let interface_def = ast.find_element::("tests::MyInterface").unwrap(); let interface_doc = interface_def.comment(); assert!(interface_doc.is_none()); } #[test] fn doc_comments_must_start_with_exactly_3_slashes() { // Arrange let slice = " module Test //// This is not a doc comment. struct Foo {} "; // Act let ast = parse_for_ast(slice); // Assert let struct_def = ast.find_element::("Test::Foo").unwrap(); let struct_doc = struct_def.comment(); assert!(struct_doc.is_none()); } #[test] fn doc_comment_linked_identifiers() { // Arrange let slice = " module tests /// This comment is for {@link TestStruct} struct TestStruct {} "; // Act let ast = parse_for_ast(slice); // Assert let struct_def = ast.find_element::("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() { // Arrange let slice = " module tests /// A test struct. Similar to {@linked OtherStruct}{}. struct TestStruct {} "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests /// A test struct. Similar to {@link OtherStruct}. struct TestStruct {} "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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) { // Arrange let slice = format!( " module tests /// A test struct, should probably use {{@link {link_identifier}}}. struct TestStruct {{}} " ); // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests interface I { /// @param foo: this parameter doesn't exist. op() } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests interface I { /// @param foo: this parameter doesn't exist. op(bar: bool) } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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) { // Arrange let slice = format!( " module tests interface I {{ /// @{returns_tag}: this tag is invalid. op() }} ", ); // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests interface I { /// @returns foo: this tag is invalid. op() -> bool } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests interface I { /// @returns foo: this tag is invalid. op() -> (alice: bool, bob: bool) } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange 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 } "; // Act let ast = parse_multiple_for_ast(&[slice1, slice2]); // Assert let operation = ast.find_element::("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() { // Arrange let slice = format!( " mode = Slice1 module tests exception Foo {{}} interface I {{ /// @throws Foo: this tag is invalid. op() }} ", ); // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " mode = Slice1 module tests interface I { /// @throws I: this type isn't an exception. op() } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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) { // Arrange 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} }} " ); // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " mode = Slice1 module tests exception E1 {} exception E2 {} interface I { /// @throws E1: first exception. /// @throws E2: second exception. op() throws (E1, E2) } "; // Act/Assert assert_parses(slice); } #[test] fn throws_tag_can_be_derived_exception() { // Arrange 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 } "; // Act/Assert assert_parses(slice); } #[test] fn throws_tag_cannot_be_base_exception() { // Arrange 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 } "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests /// @param foo: bad tag. struct Foo {} "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " module tests /// @returns: bad tag. struct Foo {} "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert 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() { // Arrange let slice = " mode = Slice1 module tests exception E {} /// @throws E: bad tag. interface Foo {} "; // Act let diagnostics = parse_for_diagnostics(slice); // Assert let expected = Diagnostic::new(Lint::IncorrectDocComment { message: "comment has a 'throws' tag, but only operations can throw".to_owned(), }); check_diagnostics(diagnostics, [expected]); } }