| // Copyright 2017-2020 The Verible Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "common/analysis/lint_rule_status.h" | |
| #include <sstream> // IWYU pragma: keep // for ostringstream | |
| #include <string> | |
| #include <vector> | |
| #include "absl/strings/str_split.h" | |
| #include "absl/strings/string_view.h" | |
| #include "common/text/concrete_syntax_tree.h" | |
| #include "common/text/token_info.h" | |
| #include "common/text/tree_builder_test_util.h" | |
| #include "gtest/gtest.h" | |
| namespace verible { | |
| namespace { | |
| // Tests initialization of LintRuleStatus. | |
| TEST(LintRuleStatusTest, Construction) { | |
| std::set<LintViolation> violations; | |
| LintRuleStatus status(violations, "RULE_NAME", "http://example.com/svstyle"); | |
| EXPECT_TRUE(status.violations.empty()); | |
| EXPECT_EQ(status.lint_rule_name, "RULE_NAME"); | |
| EXPECT_EQ(status.url, "http://example.com/svstyle"); | |
| EXPECT_TRUE(status.isOk()); | |
| } | |
| // Tests adding violations to LintRuleStatus. | |
| TEST(LintRuleStatusTest, ConstructWithViolation) { | |
| const TokenInfo token(1, "1bad-id"); | |
| std::set<LintViolation> violations({LintViolation(token, "invalid id")}); | |
| LintRuleStatus status(violations, "RULE_NAME", "http://example.com/svstyle"); | |
| EXPECT_FALSE(status.violations.empty()); | |
| EXPECT_FALSE(status.isOk()); | |
| } | |
| // Tests waiving violations and removing them from LintRuleStatus. | |
| TEST(LintRuleStatusTest, WaiveViolations) { | |
| const TokenInfo token(1, "1bad-id"); | |
| std::set<LintViolation> violations({LintViolation(token, "invalid id")}); | |
| LintRuleStatus status(violations, "RULE_NAME", "http://example.com/svstyle"); | |
| EXPECT_FALSE(status.violations.empty()); | |
| EXPECT_FALSE(status.isOk()); | |
| // First, waive nothing. | |
| status.WaiveViolations([](const LintViolation&) { return false; }); | |
| EXPECT_FALSE(status.violations.empty()); | |
| EXPECT_FALSE(status.isOk()); | |
| // Second, waive everything. | |
| status.WaiveViolations([](const LintViolation&) { return true; }); | |
| EXPECT_TRUE(status.violations.empty()); | |
| EXPECT_TRUE(status.isOk()); | |
| } | |
| // Struct for checking expected formatting of a single Lint Violation | |
| // Note that the filename produced by formatter is provided by LintStatusTest, | |
| // which contains this struct. | |
| struct LintViolationTest { | |
| std::string reason; | |
| TokenInfo token; | |
| std::string expected_output; | |
| }; | |
| // Struct for checking expected formatting of a LintRuleStatus | |
| // TODO(b/136092807): leverage SynthesizedLexerTestData to produce | |
| // expected findings. | |
| struct LintStatusTest { | |
| absl::string_view rule_name; | |
| std::string url; | |
| absl::string_view path; | |
| absl::string_view text; | |
| std::vector<LintViolationTest> violations; | |
| }; | |
| void RunLintStatusTest(const LintStatusTest& test) { | |
| // Dummy tree so we have something for test cases to point at | |
| SymbolPtr root = Node(); | |
| LintRuleStatus status; | |
| status.url = test.url; | |
| status.lint_rule_name = test.rule_name; | |
| for (const auto& violation_test : test.violations) { | |
| status.violations.insert( | |
| LintViolation(violation_test.token, violation_test.reason)); | |
| } | |
| std::ostringstream ss; | |
| LintStatusFormatter formatter(test.text); | |
| formatter.FormatLintRuleStatus(&ss, status, test.text, test.path); | |
| auto result_parts = absl::StrSplit(ss.str(), '\n'); | |
| auto part_iterator = result_parts.begin(); | |
| for (const auto& violation_test : test.violations) { | |
| EXPECT_EQ(*part_iterator, violation_test.expected_output); | |
| part_iterator++; | |
| } | |
| } | |
| TEST(LintRuleStatusFormatterTest, SimpleOutput) { | |
| SymbolPtr root = Node(); | |
| static const int dont_care_tag = 0; | |
| constexpr absl::string_view text( | |
| "This is some code\n" | |
| "That you are looking at right now\n" | |
| "It is nice code, make no mistake\n" | |
| "Very nice"); | |
| LintStatusTest test = { | |
| "test-rule", | |
| "http://foobar", | |
| "some/path/to/somewhere.fvg", | |
| text, | |
| {{"reason1", TokenInfo(dont_care_tag, text.substr(0, 5)), | |
| "some/path/to/somewhere.fvg:1:1-5: reason1 http://foobar [test-rule]"}, | |
| {"reason2", TokenInfo(dont_care_tag, text.substr(21, 4)), | |
| "some/path/to/somewhere.fvg:2:4-7: reason2 http://foobar " | |
| "[test-rule]"}}}; | |
| RunLintStatusTest(test); | |
| } | |
| TEST(LintRuleStatusFormatterTest, NoOutput) { | |
| SymbolPtr root = Node(); | |
| LintStatusTest test = {"cool-rule", | |
| "http://example.com/svstyle", | |
| "some/path/to/somewhere.fvg", | |
| "This is some code\n" | |
| "That you are looking at right now\n" | |
| "It is nice code, make no mistake\n" | |
| "Very nice", | |
| {}}; | |
| RunLintStatusTest(test); | |
| } | |
| void RunLintStatusesTest(const LintStatusTest& test, bool show_context) { | |
| // Dummy tree so we have something for test cases to point at | |
| SymbolPtr root = Node(); | |
| std::vector<LintRuleStatus> statuses; | |
| LintRuleStatus status0; | |
| status0.url = test.url; | |
| status0.lint_rule_name = test.rule_name; | |
| LintRuleStatus status1; | |
| status1.url = test.url; | |
| status1.lint_rule_name = test.rule_name; | |
| ASSERT_EQ(test.violations.size(), 2); | |
| // Insert the violations in the wrong order | |
| status0.violations.insert( | |
| LintViolation(test.violations[1].token, test.violations[1].reason)); | |
| status1.violations.insert( | |
| LintViolation(test.violations[0].token, test.violations[0].reason)); | |
| statuses.push_back(status0); | |
| statuses.push_back(status1); | |
| std::ostringstream ss; | |
| LintStatusFormatter formatter(test.text); | |
| const std::vector<absl::string_view> lines; | |
| if (!show_context) { | |
| formatter.FormatLintRuleStatuses(&ss, statuses, test.text, test.path, {}); | |
| } else { | |
| formatter.FormatLintRuleStatuses(&ss, statuses, test.text, test.path, | |
| absl::StrSplit(test.text, '\n')); | |
| std::cout << ss.str() << std::endl; | |
| } | |
| std::vector<std::string> result_parts; | |
| if (!show_context) { | |
| result_parts = absl::StrSplit(ss.str(), '\n'); | |
| } else { | |
| result_parts = absl::StrSplit(ss.str(), "^\n"); | |
| } | |
| auto part_iterator = result_parts.begin(); | |
| for (const auto& violation_test : test.violations) { | |
| EXPECT_EQ(*part_iterator, violation_test.expected_output); | |
| part_iterator++; | |
| } | |
| } | |
| TEST(LintRuleStatusFormatterTest, MultipleStatusesSimpleOutput) { | |
| SymbolPtr root = Node(); | |
| static const int dont_care_tag = 0; | |
| constexpr absl::string_view text( | |
| "This is some code\n" | |
| "That you are looking at right now\n" | |
| "It is nice code, make no mistake\n" | |
| "Very nice"); | |
| LintStatusTest test = { | |
| "test-rule", | |
| "http://foobar", | |
| "some/path/to/somewhere.fvg", | |
| text, | |
| {{"reason1", TokenInfo(dont_care_tag, text.substr(0, 5)), | |
| "some/path/to/somewhere.fvg:1:1-5: reason1 http://foobar [test-rule]"}, | |
| {"reason2", TokenInfo(dont_care_tag, text.substr(21, 4)), | |
| "some/path/to/somewhere.fvg:2:4-7: reason2 http://foobar " | |
| "[test-rule]"}}}; | |
| RunLintStatusesTest(test, false); | |
| } | |
| TEST(LintRuleStatusFormatterTestWithContext, MultipleStatusesSimpleOutput) { | |
| SymbolPtr root = Node(); | |
| static const int dont_care_tag = 0; | |
| constexpr absl::string_view text( | |
| "This is some code\n" | |
| "That you are looking at right now\n" | |
| "It is nice code, make no mistake\n" | |
| "Very nice"); | |
| LintStatusTest test = { | |
| "test-rule", | |
| "http://foobar", | |
| "some/path/to/somewhere.fvg", | |
| text, | |
| {{"reason1", TokenInfo(dont_care_tag, text.substr(0, 5)), | |
| "some/path/to/somewhere.fvg:1:1-5: reason1 http://foobar " | |
| "[test-rule]\nThis is some code\n"}, | |
| {"reason2", TokenInfo(dont_care_tag, text.substr(21, 4)), | |
| "some/path/to/somewhere.fvg:2:4-7: reason2 http://foobar " | |
| "[test-rule]\nThat you are looking at right now\n "}}}; | |
| RunLintStatusesTest(test, true); | |
| } | |
| TEST(LintRuleStatusFormatterTestWithContext, PointToCorrectUtf8Char) { | |
| SymbolPtr root = Node(); | |
| static const int dont_care_tag = 0; | |
| constexpr absl::string_view text("äöüß\n"); | |
| // ^ä^ü | |
| LintStatusTest test = { | |
| "rule", | |
| "URL", | |
| "some/file.sv", | |
| text, | |
| {{"reason1", TokenInfo(dont_care_tag, text.substr(0, 2)), | |
| "some/file.sv:1:1: reason1 URL " | |
| "[rule]\näöüß\n"}, | |
| {"reason2", TokenInfo(dont_care_tag, text.substr(strlen("äö"), 2)), | |
| "some/file.sv:1:3: reason2 URL " | |
| "[rule]\näöüß\n "}}}; | |
| RunLintStatusesTest(test, true); | |
| } | |
| TEST(AutoFixTest, ValidUseCases) { | |
| // 0123456789abcdef | |
| static constexpr absl::string_view text("This is an image"); | |
| // AutoFix(ReplacementEdit) | |
| const AutoFix singleEdit("e", {text.substr(5, 2), "isn't"}); | |
| EXPECT_EQ(singleEdit.Apply(text), "This isn't an image"); | |
| const AutoFix singleInsert("i", {text.substr(16, 0), "."}); | |
| EXPECT_EQ(singleInsert.Apply(text), "This is an image."); | |
| // AutoFix() | |
| AutoFix fixesCollection; | |
| EXPECT_TRUE(fixesCollection.AddEdits(singleEdit.Edits())); | |
| EXPECT_TRUE(fixesCollection.AddEdits(singleInsert.Edits())); | |
| EXPECT_EQ(fixesCollection.Apply(text), "This isn't an image."); | |
| EXPECT_TRUE(fixesCollection.AddEdits({{text.substr(0, 0), "Hello. "}})); | |
| EXPECT_EQ(fixesCollection.Apply(text), "Hello. This isn't an image."); | |
| static const int dont_care_tag = 0; | |
| TokenInfo image_token(dont_care_tag, text.substr(11, 5)); | |
| // AutoFix(ReplacementEdit), | |
| // ReplacementEdit(const TokenInfo&, const std::string&) | |
| AutoFix otherCollection("image", {image_token, "🖼"}); | |
| EXPECT_TRUE(otherCollection.AddEdits(fixesCollection.Edits())); | |
| EXPECT_EQ(otherCollection.Apply(text), "Hello. This isn't an 🖼."); | |
| // AutoFix(std::initializer_list<ReplacementEdit>) | |
| const AutoFix multipleEdits("Multi-edit", { | |
| {text.substr(11, 5), "text"}, | |
| {text.substr(8, 2), "a"}, | |
| }); | |
| EXPECT_EQ(multipleEdits.Apply(text), "This is a text"); | |
| // AutoFix(const AutoFix& other) | |
| AutoFix copyFix(multipleEdits); | |
| EXPECT_EQ(copyFix.Apply(text), "This is a text"); | |
| EXPECT_TRUE(copyFix.AddEdits(singleInsert.Edits())); | |
| EXPECT_EQ(copyFix.Apply(text), "This is a text."); | |
| // AutoFix(const AutoFix&& other) | |
| AutoFix moveFix(std::move(copyFix)); | |
| EXPECT_EQ(moveFix.Apply(text), "This is a text."); | |
| // AutoFix(const std::string&, ReplacementEdit) | |
| const AutoFix singleInsertWithDescription("Add dot", | |
| {text.substr(16, 0), "."}); | |
| EXPECT_EQ(singleInsertWithDescription.Apply(text), "This is an image."); | |
| EXPECT_EQ(singleInsertWithDescription.Description(), "Add dot"); | |
| // AutoFix(const std::string&, std::initializer_list<ReplacementEdit>) | |
| const AutoFix multipleEditsWithDescription("Stop lying", | |
| { | |
| {text.substr(11, 5), "text"}, | |
| {text.substr(8, 2), "a"}, | |
| }); | |
| EXPECT_EQ(multipleEditsWithDescription.Apply(text), "This is a text"); | |
| EXPECT_EQ(multipleEditsWithDescription.Description(), "Stop lying"); | |
| } | |
| TEST(AutoFixTest, ConflictingEdits) { | |
| // 0123456789abcdef | |
| static constexpr absl::string_view text("This is an image"); | |
| AutoFix fixesCollection; | |
| EXPECT_TRUE(fixesCollection.AddEdits({{text.substr(8, 8), "a text"}})); | |
| EXPECT_FALSE(fixesCollection.AddEdits({{text.substr(11, 5), "IMAGE"}})); | |
| EXPECT_FALSE(fixesCollection.AddEdits({{text.substr(8, 1), "A"}})); | |
| EXPECT_FALSE(fixesCollection.AddEdits({{text.substr(15, 1), "ination"}})); | |
| EXPECT_EQ(fixesCollection.Apply(text), "This is a text"); | |
| EXPECT_DEATH(AutoFix("overlap", {{text.substr(8, 8), "a text"}, // | |
| {text.substr(11, 5), "IMAGE"}}), | |
| "Edits must not overlap"); | |
| EXPECT_DEATH(AutoFix("overlap", {{text.substr(8, 8), "a text"}, // | |
| {text.substr(8, 1), "A"}}), | |
| "Edits must not overlap"); | |
| EXPECT_DEATH(AutoFix("overlap", {{text.substr(8, 8), "a text"}, // | |
| {text.substr(15, 1), "ination"}}), | |
| "Edits must not overlap"); | |
| } | |
| TEST(LintViolationTest, ViolationWithAutoFix) { | |
| static constexpr absl::string_view text("This is an image"); | |
| static const int dont_care_tag = 0; | |
| const TokenInfo an_image_token(dont_care_tag, text.substr(8, 5)); | |
| const LintViolation no_fixes(an_image_token, "No, it's not."); | |
| EXPECT_TRUE(no_fixes.autofixes.empty()); | |
| const LintViolation single_fix( | |
| an_image_token, "No, it's not.", | |
| { | |
| AutoFix("Replace with 'a text'", {an_image_token, "a text"}), | |
| }); | |
| EXPECT_EQ(single_fix.autofixes.size(), 1); | |
| const LintViolation multiple_fixes( | |
| an_image_token, "No, it's not.", | |
| { | |
| AutoFix("Replace with 'a text'", {an_image_token, "a text"}), | |
| AutoFix("Add waiver comment", | |
| {text.substr(0, 0), "// verilog_lint: waive no-lying\n"}), | |
| }); | |
| EXPECT_EQ(multiple_fixes.autofixes.size(), 2); | |
| } | |
| } // namespace | |
| } // namespace verible |