| // 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 |