| // 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 "verilog/analysis/verilog_linter_configuration.h" | |
| #include <algorithm> | |
| #include <functional> | |
| #include <initializer_list> | |
| #include <map> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/str_split.h" | |
| #include "absl/strings/string_view.h" | |
| #include "common/analysis/line_lint_rule.h" | |
| #include "common/analysis/syntax_tree_lint_rule.h" | |
| #include "common/analysis/text_structure_lint_rule.h" | |
| #include "common/analysis/token_stream_lint_rule.h" | |
| #include "common/util/container_util.h" | |
| #include "common/util/enum_flags.h" | |
| #include "common/util/file_util.h" | |
| #include "common/util/logging.h" | |
| #include "verilog/analysis/default_rules.h" | |
| #include "verilog/analysis/lint_rule_registry.h" | |
| namespace verilog { | |
| using verible::LineLintRule; | |
| using verible::SyntaxTreeLintRule; | |
| using verible::TextStructureLintRule; | |
| using verible::TokenStreamLintRule; | |
| using verible::container::FindOrNull; | |
| template <typename List> | |
| static const char* MatchesAnyItem(absl::string_view filename, | |
| const List& items) { | |
| for (const auto item : items) { | |
| if (absl::StrContains(filename, item)) { | |
| return item; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| const char* ProjectPolicy::MatchesAnyPath(absl::string_view filename) const { | |
| return MatchesAnyItem(filename, path_substrings); | |
| } | |
| const char* ProjectPolicy::MatchesAnyExclusions( | |
| absl::string_view filename) const { | |
| return MatchesAnyItem(filename, path_exclusions); | |
| } | |
| bool ProjectPolicy::IsValid() const { | |
| for (const auto rule : disabled_rules) { | |
| if (!analysis::IsRegisteredLintRule(rule)) return false; | |
| } | |
| for (const auto rule : enabled_rules) { | |
| if (!analysis::IsRegisteredLintRule(rule)) return false; | |
| } | |
| return true; | |
| } | |
| std::string ProjectPolicy::ListPathGlobs() const { | |
| return absl::StrJoin(path_substrings.begin(), path_substrings.end(), " | ", | |
| [](std::string* out, absl::string_view pattern) { | |
| absl::StrAppend(out, "*", pattern, "*"); | |
| }); | |
| } | |
| bool RuleBundle::ParseConfiguration(absl::string_view text, char separator, | |
| std::string* error) { | |
| // Clear the vector to overwrite any existing value. | |
| rules.clear(); | |
| for (absl::string_view part : | |
| absl::StrSplit(text, separator, absl::SkipEmpty())) { | |
| if (separator == '\n') { | |
| // In configuration files, we can ignore #-comments | |
| // TODO(hzeller): this will fall short if in the configuration string | |
| // we expect rules that accept a #-character for some reason. In that | |
| // case we need to expand this to parse out 'within # quotes' parts. | |
| // ... then this will finally become a more complex lexer. | |
| const auto comment_pos = part.find('#'); | |
| if (comment_pos != absl::string_view::npos) { | |
| part = part.substr(0, comment_pos); | |
| } | |
| } | |
| part = absl::StripAsciiWhitespace(part); | |
| if (part.empty()) continue; | |
| // If prefix is '-', the rule is disabled. For symmetry, we also allow | |
| // '+' to enable rule. | |
| // Note that part is guaranteed to be at least one character because | |
| // of absl::SkipEmpty() | |
| const bool has_prefix = (part[0] == '+' || part[0] == '-'); | |
| const bool prefix_minus = (part[0] == '-'); | |
| RuleSetting setting = {!prefix_minus, ""}; | |
| const auto rule_name_with_config = part.substr(has_prefix ? 1 : 0); | |
| // Independent of the enabled-ness: extract a configuration string | |
| // if there is any assignment. | |
| const auto equals_pos = rule_name_with_config.find('='); | |
| if (equals_pos != absl::string_view::npos) { | |
| const auto config = rule_name_with_config.substr(equals_pos + 1); | |
| setting.configuration.assign(config.data(), config.size()); | |
| } | |
| const auto rule_name = rule_name_with_config.substr(0, equals_pos); | |
| const auto rule_name_set = analysis::GetAllRegisteredLintRuleNames(); | |
| const auto rule_iter = rule_name_set.find(rule_name); | |
| // Check if text is a valid lint rule. | |
| if (rule_iter == rule_name_set.end()) { | |
| *error = absl::StrCat("invalid flag \"", rule_name, "\""); | |
| return false; | |
| } else { | |
| // Map keys must use canonical registered string_views for guaranteed | |
| // lifetime, not just any string-equivalent copy. | |
| rules[*rule_iter] = setting; | |
| } | |
| } | |
| return true; | |
| } | |
| // Parse and unparse for RuleBundle (for commandlineflags) | |
| std::string RuleBundle::UnparseConfiguration(const char separator) const { | |
| std::vector<std::string> switches; | |
| for (const auto& rule : rules) { | |
| switches.push_back(absl::StrCat( | |
| // If rule is set off, prepend "-" | |
| rule.second.enabled ? "" : "-", rule.first, | |
| // If we have a configuration, append assignment. | |
| rule.second.configuration.empty() ? "" : "=", | |
| rule.second.configuration)); | |
| } | |
| // Concatenates all of rules into text. | |
| return absl::StrJoin(switches.rbegin(), switches.rend(), | |
| std::string(1, separator)); | |
| } | |
| bool LinterConfiguration::RuleIsOn(const analysis::LintRuleId& rule) const { | |
| const auto* entry = FindOrNull(configuration_, rule); | |
| if (entry == nullptr) return false; | |
| return entry->enabled; | |
| } | |
| void LinterConfiguration::UseRuleSet(const RuleSet& rules) { | |
| configuration_.clear(); | |
| switch (rules) { | |
| case RuleSet::kAll: { | |
| for (const auto& rule : analysis::RegisteredTextStructureRulesNames()) { | |
| TurnOn(rule); | |
| } | |
| for (const auto& rule : analysis::RegisteredSyntaxTreeRulesNames()) { | |
| TurnOn(rule); | |
| } | |
| for (const auto& rule : analysis::RegisteredTokenStreamRulesNames()) { | |
| TurnOn(rule); | |
| } | |
| for (const auto& rule : analysis::RegisteredLineRulesNames()) { | |
| TurnOn(rule); | |
| } | |
| break; | |
| } | |
| case RuleSet::kNone: | |
| break; | |
| case RuleSet::kDefault: | |
| for (const auto& rule : analysis::kDefaultRuleSet) { | |
| TurnOn(rule); | |
| } | |
| } | |
| } | |
| void LinterConfiguration::UseRuleBundle(const RuleBundle& rule_bundle) { | |
| const auto name_set = analysis::GetAllRegisteredLintRuleNames(); | |
| for (const auto& rule_pair : rule_bundle.rules) { | |
| // This needs to use the canonical registered key string_view, which has | |
| // guaranteed lifetime. | |
| configuration_[rule_pair.first] = rule_pair.second; | |
| } | |
| } | |
| void LinterConfiguration::UseProjectPolicy(const ProjectPolicy& policy, | |
| absl::string_view filename) { | |
| if (const char* matched_path = policy.MatchesAnyPath(filename)) { | |
| VLOG(1) << "File \"" << filename << "\" matches path \"" << matched_path | |
| << "\" from project policy [" << policy.name << "], applying it."; | |
| for (const auto rule : policy.disabled_rules) { | |
| VLOG(1) << " disabling rule: " << rule; | |
| TurnOff(rule); | |
| } | |
| for (const auto rule : policy.enabled_rules) { | |
| VLOG(1) << " enabling rule: " << rule; | |
| TurnOn(rule); | |
| } | |
| } | |
| } | |
| std::set<analysis::LintRuleId> LinterConfiguration::ActiveRuleIds() const { | |
| std::set<analysis::LintRuleId> result; | |
| for (const auto& rule_pair : configuration_) { | |
| if (rule_pair.second.enabled) { | |
| result.insert(rule_pair.first); | |
| } | |
| } | |
| return result; | |
| } | |
| // Iterates through all rules that are mentioned and enabled | |
| // in the "config" map. Constructs instances using the | |
| // "factory"-function, and configures them if a configuration string is | |
| // available. Returns a vector of all successfully created instances. | |
| // | |
| // T should be a descendant of verible::LintRule. | |
| template <typename T> | |
| static std::vector<std::unique_ptr<T>> CreateRules( | |
| const std::map<analysis::LintRuleId, RuleSetting>& config, | |
| std::function<std::unique_ptr<T>(const analysis::LintRuleId&)> factory) { | |
| std::vector<std::unique_ptr<T>> rule_instances; | |
| for (const auto& rule_pair : config) { | |
| const RuleSetting& setting = rule_pair.second; | |
| if (!setting.enabled) continue; | |
| std::unique_ptr<T> rule_ptr = factory(rule_pair.first); | |
| if (rule_ptr == nullptr) continue; | |
| if (!setting.configuration.empty()) { | |
| absl::Status status; | |
| if (!(status = rule_ptr->Configure(setting.configuration)).ok()) { | |
| // TODO(hzeller): return error message to caller to handle ? | |
| LOG(QFATAL) << rule_pair.first << ": " << status.message(); | |
| } | |
| } | |
| rule_instances.push_back(std::move(rule_ptr)); | |
| } | |
| return rule_instances; | |
| } | |
| std::vector<std::unique_ptr<SyntaxTreeLintRule>> | |
| LinterConfiguration::CreateSyntaxTreeRules() const { | |
| return CreateRules<SyntaxTreeLintRule>(configuration_, | |
| analysis::CreateSyntaxTreeLintRule); | |
| } | |
| std::vector<std::unique_ptr<TokenStreamLintRule>> | |
| LinterConfiguration::CreateTokenStreamRules() const { | |
| return CreateRules<TokenStreamLintRule>(configuration_, | |
| analysis::CreateTokenStreamLintRule); | |
| } | |
| std::vector<std::unique_ptr<LineLintRule>> | |
| LinterConfiguration::CreateLineRules() const { | |
| return CreateRules<LineLintRule>(configuration_, | |
| analysis::CreateLineLintRule); | |
| } | |
| std::vector<std::unique_ptr<TextStructureLintRule>> | |
| LinterConfiguration::CreateTextStructureRules() const { | |
| return CreateRules<TextStructureLintRule>( | |
| configuration_, analysis::CreateTextStructureLintRule); | |
| } | |
| bool LinterConfiguration::operator==(const LinterConfiguration& config) const { | |
| return ActiveRuleIds() == config.ActiveRuleIds(); | |
| } | |
| absl::Status LinterConfiguration::AppendFromFile( | |
| absl::string_view config_filename) { | |
| // Read local configuration file | |
| std::string content; | |
| const absl::Status config_read_status = | |
| verible::file::GetContents(config_filename, &content); | |
| if (config_read_status.ok()) { | |
| RuleBundle local_rules_bundle; | |
| std::string error; | |
| if (local_rules_bundle.ParseConfiguration(content, '\n', &error)) { | |
| UseRuleBundle(local_rules_bundle); | |
| } else { | |
| LOG(WARNING) << "Unable to fully parse configuration: " << error; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| return config_read_status; | |
| } | |
| absl::Status LinterConfiguration::ConfigureFromOptions( | |
| const LinterOptions& options) { | |
| UseRuleSet(options.ruleset); | |
| if (options.config_file_is_custom) { | |
| const absl::Status config_read_status = AppendFromFile(options.config_file); | |
| if (!config_read_status.ok()) { | |
| LOG(WARNING) << options.config_file | |
| << ": Unable to read rules configuration file " | |
| << config_read_status << std::endl; | |
| } | |
| } else if (options.rules_config_search) { | |
| // Search upward if search is enabled and no configuration file is | |
| // specified | |
| static constexpr absl::string_view linter_config = ".rules.verible_lint"; | |
| std::string resolved_config_file; | |
| if (verible::file::UpwardFileSearch(options.linting_start_file, | |
| linter_config, &resolved_config_file) | |
| .ok()) { | |
| const absl::Status config_read_status = | |
| AppendFromFile(resolved_config_file); | |
| if (!config_read_status.ok()) { | |
| LOG(WARNING) << resolved_config_file | |
| << ": Unable to read rules configuration file " | |
| << config_read_status << std::endl; | |
| } | |
| } | |
| } | |
| // Turn on rules found in config | |
| UseRuleBundle(options.rules); | |
| // Apply external waivers | |
| external_waivers = std::string(options.waiver_files); | |
| return absl::OkStatus(); | |
| } | |
| std::ostream& operator<<(std::ostream& stream, | |
| const LinterConfiguration& config) { | |
| const auto rules = config.ActiveRuleIds(); | |
| return stream << "{ " << absl::StrJoin(rules.begin(), rules.end(), ", ") | |
| << " }"; | |
| } | |
| static constexpr std::initializer_list< | |
| std::pair<const absl::string_view, RuleSet>> | |
| kRuleSetEnumStringMap = { | |
| {"all", RuleSet::kAll}, | |
| {"none", RuleSet::kNone}, | |
| {"default", RuleSet::kDefault}, | |
| }; | |
| std::ostream& operator<<(std::ostream& stream, RuleSet rules) { | |
| static const auto* flag_map = | |
| verible::MakeEnumToStringMap(kRuleSetEnumStringMap); | |
| return stream << flag_map->find(rules)->second; | |
| } | |
| // | |
| // Parse and unparse for ruleset (for commandlineflags) | |
| // | |
| std::string AbslUnparseFlag(const RuleSet& rules) { | |
| std::ostringstream stream; | |
| stream << rules; | |
| return stream.str(); | |
| } | |
| bool AbslParseFlag(absl::string_view text, RuleSet* rules, std::string* error) { | |
| static const auto* flag_map = | |
| verible::MakeStringToEnumMap(kRuleSetEnumStringMap); | |
| return EnumMapParseFlag(*flag_map, text, rules, error); | |
| } | |
| std::string AbslUnparseFlag(const RuleBundle& bundle) { | |
| return bundle.UnparseConfiguration(','); | |
| } | |
| bool AbslParseFlag(absl::string_view text, RuleBundle* bundle, | |
| std::string* error) { | |
| return bundle->ParseConfiguration(text, ',', error); | |
| } | |
| } // namespace verilog |