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