| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <fidl/linter.h> |
| |
| #include <algorithm> |
| #include <iostream> |
| #include <regex> |
| #include <set> |
| |
| #include <lib/fit/function.h> |
| |
| #include <fidl/findings.h> |
| #include <fidl/raw_ast.h> |
| #include <fidl/utils.h> |
| |
| namespace fidl { |
| namespace linter { |
| |
| const std::set<std::string>& Linter::permitted_library_prefixes() const { |
| return permitted_library_prefixes_; |
| } |
| |
| std::string Linter::permitted_library_prefixes_as_string() const { |
| std::ostringstream ss; |
| bool first = true; |
| for (auto& prefix : permitted_library_prefixes()) { |
| if (!first) { |
| ss << " | "; |
| } |
| ss << prefix; |
| first = false; |
| } |
| return ss.str(); |
| } |
| |
| // Returns itself. Overloaded to support alternative type references by |
| // pointer and unique_ptr as needed. |
| static const fidl::raw::SourceElement& GetElementAsRef( |
| const fidl::raw::SourceElement& source_element) { |
| return source_element; |
| } |
| |
| static const fidl::raw::SourceElement& GetElementAsRef( |
| const fidl::raw::SourceElement* element) { |
| return GetElementAsRef(*element); |
| } |
| |
| // Returns the pointed-to element as a reference. |
| template <typename SourceElementSubtype> |
| const fidl::raw::SourceElement& GetElementAsRef( |
| const std::unique_ptr<SourceElementSubtype>& element_ptr) { |
| static_assert( |
| std::is_base_of<fidl::raw::SourceElement, SourceElementSubtype>::value, |
| "Template parameter type is not derived from SourceElement"); |
| return GetElementAsRef(element_ptr.get()); |
| } |
| |
| // Convert the SourceElement (start- and end-tokens within the SourceFile) |
| // to a std::string_view, spanning from the beginning of the start token, to the end |
| // of the end token. The three methods support classes derived from |
| // SourceElement, by reference, pointer, or unique_ptr. |
| static std::string_view to_string_view(const fidl::raw::SourceElement& element) { |
| auto start_string = element.start_.data(); |
| const char* start_ptr = start_string.data(); |
| auto end_string = element.end_.data(); |
| const char* end_ptr = end_string.data() + end_string.size(); |
| size_t size = static_cast<size_t>(end_ptr - start_ptr); |
| return std::string_view(start_ptr, size); |
| } |
| |
| static std::string_view to_string_view(const fidl::raw::SourceElement* element) { |
| return to_string_view(*element); |
| } |
| |
| template <typename SourceElementSubtype> |
| std::string_view to_string_view( |
| const std::unique_ptr<SourceElementSubtype>& element_ptr) { |
| static_assert( |
| std::is_base_of<fidl::raw::SourceElement, SourceElementSubtype>::value, |
| "Template parameter type is not derived from SourceElement"); |
| return to_string_view(element_ptr.get()); |
| } |
| |
| // Convert the SourceElement to a std::string, using the method described above |
| // for std::string_view. |
| static std::string to_string(const fidl::raw::SourceElement& element) { |
| return std::string(to_string_view(element)); |
| } |
| |
| static std::string to_string(const fidl::raw::SourceElement* element) { |
| return std::string(to_string_view(*element)); |
| } |
| |
| template <typename SourceElementSubtype> |
| std::string to_string( |
| const std::unique_ptr<SourceElementSubtype>& element_ptr) { |
| static_assert( |
| std::is_base_of<fidl::raw::SourceElement, SourceElementSubtype>::value, |
| "Template parameter type is not derived from SourceElement"); |
| return to_string(element_ptr.get()); |
| } |
| |
| // Add a finding with |Finding| constructor arguments. |
| // This function is const because the Findings (TreeVisitor) object |
| // is not modified. It's Findings object (not owned) is updated. |
| template <typename... Args> |
| Finding& Linter::AddFinding(Args&&... args) const { |
| assert(current_findings_ != nullptr); |
| return current_findings_->emplace_back(std::forward<Args>(args)...); |
| } |
| |
| // Add a finding with optional suggestion and replacement |
| const Finding& Linter::AddFinding( |
| SourceLocation location, |
| const CheckDef& check, |
| Substitutions substitutions, |
| std::string suggestion_template, |
| std::string replacement_template) const { |
| auto& finding = AddFinding( |
| location, |
| check.id(), check.message_template().Substitute(substitutions)); |
| if (suggestion_template.size() > 0) { |
| if (replacement_template.size() == 0) { |
| finding.SetSuggestion( |
| TemplateString(suggestion_template).Substitute(substitutions)); |
| } else { |
| finding.SetSuggestion( |
| TemplateString(suggestion_template).Substitute(substitutions), |
| TemplateString(replacement_template).Substitute(substitutions)); |
| } |
| } |
| return finding; |
| } |
| |
| // Add a finding from a SourceElement |
| template <typename SourceElementSubtypeRefOrPtr> |
| const Finding& Linter::AddFinding( |
| const SourceElementSubtypeRefOrPtr& element, |
| const CheckDef& check, |
| Substitutions substitutions, |
| std::string suggestion_template, |
| std::string replacement_template) const { |
| auto& finding = AddFinding( |
| GetElementAsRef(element).location(), |
| check, substitutions, |
| suggestion_template, replacement_template); |
| return finding; |
| } |
| |
| CheckDef Linter::DefineCheck(std::string check_id, |
| std::string message_template) { |
| return *checks_.emplace(check_id, TemplateString(message_template)).first; |
| } |
| |
| // Returns true if no new findings were generated |
| bool Linter::Lint(std::unique_ptr<raw::File> const& parsed_source, |
| Findings* findings) { |
| size_t initial_findings_count = findings->size(); |
| current_findings_ = findings; |
| callbacks_.Visit(parsed_source); |
| current_findings_ = nullptr; |
| if (findings->size() == initial_findings_count) { |
| return true; |
| } |
| return false; |
| } |
| |
| const Finding* Linter::CheckCase( |
| std::string type, const std::unique_ptr<raw::Identifier>& identifier, |
| const CheckDef& check_def, const CaseType& case_type) { |
| std::string id = to_string(identifier); |
| if (!case_type.matches(id)) { |
| return &AddFinding( |
| identifier, check_def, |
| { |
| {"TYPE", type}, |
| {"IDENTIFIER", id}, |
| {"REPLACEMENT", case_type.convert(id)}, |
| }, |
| "change '${IDENTIFIER}' to '${REPLACEMENT}'", |
| "${REPLACEMENT}"); |
| } |
| return nullptr; |
| } |
| |
| void Linter::CheckRepeatedName( |
| std::string type, const std::unique_ptr<raw::Identifier>& identifier) { |
| std::string id = to_string(identifier); |
| auto split_id = utils::id_to_words(id, stop_words_); |
| std::set<std::string> words; |
| words.insert(split_id.begin(), split_id.end()); |
| for (auto& context : context_stack_) { |
| std::set<std::string> repeats; |
| std::set_intersection(words.begin(), words.end(), |
| context.words().begin(), context.words().end(), |
| std::inserter(repeats, repeats.begin())); |
| if (!repeats.empty()) { |
| context.AddRepeatsContextNames(type, identifier->location(), repeats); |
| } |
| } |
| } |
| |
| const Finding& Linter::AddRepeatedNameFinding( |
| const Context& context, |
| const Context::RepeatsContextNames& name_repeater) const { |
| std::string repeated_names; |
| for (const auto& repeat : name_repeater.repeats) { |
| if (!repeated_names.empty()) { |
| repeated_names.append(", "); |
| } |
| repeated_names.append(repeat); |
| } |
| return AddFinding( |
| name_repeater.location, context.context_check(), |
| { |
| {"TYPE", name_repeater.type}, |
| {"REPEATED_NAMES", repeated_names}, |
| {"CONTEXT_TYPE", context.type()}, |
| {"CONTEXT_ID", context.id()}, |
| }); |
| } |
| |
| void Linter::ExitContext() { |
| Context context = std::move(context_stack_.front()); |
| context_stack_.pop_front(); |
| |
| // Check the |RepeatsContextNames| objects in context.name_repeaters(), and |
| // produce Finding objects for any identifier that is not allowed to repeat |
| // a name from this |Context|. |
| // |
| // This check addresses the FIDL Rubric rule: |
| // |
| // Member names must not repeat names from the enclosing type (or |
| // library) unless the member name is ambiguous without a name from |
| // the enclosing type... |
| // |
| // ...a type DeviceToRoom--that associates a smart device with the room |
| // it's located in--may need to have members device_id and room_name, |
| // because id and name are ambiguous; they could refer to either the |
| // device or the room. |
| auto& repeaters = context.name_repeaters(); |
| for (size_t i = 1; i < repeaters.size(); i++) { |
| std::set<std::string> diffs; |
| std::set_difference(repeaters[i - 1].repeats.begin(), repeaters[i - 1].repeats.end(), |
| repeaters[i].repeats.begin(), repeaters[i].repeats.end(), |
| std::inserter(diffs, diffs.begin())); |
| if (!diffs.empty()) { |
| // If there are any differences, we have to assume they may be |
| // disambiguating, which is allowed. |
| return; |
| } |
| } |
| // If multiple name repeaters in a given context all repeat the same thing, |
| // then it's obvious they don't disambiguate anything, so add Findings for |
| // each violator. |
| for (auto& repeater : repeaters) { |
| AddRepeatedNameFinding(context, repeater); |
| } |
| } |
| |
| static std::string to_library_id(const std::vector<std::unique_ptr<raw::Identifier>>& components) { |
| std::string id; |
| for (const auto& component : components) { |
| if (!id.empty()) { |
| id.append("."); |
| } |
| id.append(to_string(component)); |
| } |
| return id; |
| } |
| |
| Linter::Linter() |
| : callbacks_(LintingTreeCallbacks()), |
| permitted_library_prefixes_({ |
| "fuchsia", |
| "fidl", |
| "test", |
| }), |
| stop_words_({ |
| "a", |
| "about", |
| "above", |
| "after", |
| "against", |
| "all", |
| "an", |
| "and", |
| "any", |
| "are", |
| "as", |
| "at", |
| "be", |
| "because", |
| "been", |
| "before", |
| "being", |
| "below", |
| "between", |
| "both", |
| "but", |
| "by", |
| "can", |
| "did", |
| "do", |
| "does", |
| "doing", |
| "down", |
| "during", |
| "each", |
| "few", |
| "for", |
| "from", |
| "further", |
| "had", |
| "has", |
| "have", |
| "having", |
| "here", |
| "how", |
| "if", |
| "in", |
| "into", |
| "is", |
| "it", |
| "its", |
| "itself", |
| "just", |
| "more", |
| "most", |
| "no", |
| "nor", |
| "not", |
| "now", |
| "of", |
| "off", |
| "on", |
| "once", |
| "only", |
| "or", |
| "other", |
| "out", |
| "over", |
| "own", |
| "same", |
| "should", |
| "so", |
| "some", |
| "such", |
| "than", |
| "that", |
| "the", |
| "then", |
| "there", |
| "these", |
| "this", |
| "those", |
| "through", |
| "to", |
| "too", |
| "under", |
| "until", |
| "up", |
| "very", |
| "was", |
| "were", |
| "what", |
| "when", |
| "where", |
| "which", |
| "while", |
| "why", |
| "will", |
| "with", |
| }) { |
| |
| callbacks_.OnUsing( |
| [& linter = *this, |
| case_check = DefineCheck( |
| "invalid-case-for-primitive-alias", |
| "Primitive aliases must be named in lower_snake_case"), |
| &case_type = lower_snake_] |
| // |
| (const raw::Using& element) { |
| if (element.maybe_alias != nullptr) { |
| linter.CheckCase("primitive alias", element.maybe_alias, |
| case_check, case_type); |
| linter.CheckRepeatedName("primitive alias", element.maybe_alias); |
| } |
| }); |
| |
| auto invalid_case_for_constant = DefineCheck( |
| "invalid-case-for-constant", |
| "${TYPE} must be named in ALL_CAPS_SNAKE_CASE"); |
| |
| callbacks_.OnConstDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_constant, |
| &case_type = upper_snake_] |
| // |
| (const raw::ConstDeclaration& element) { |
| linter.CheckCase("constants", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("constant", element.identifier); |
| }); |
| |
| callbacks_.OnEnumMember( |
| [& linter = *this, |
| case_check = invalid_case_for_constant, |
| &case_type = upper_snake_] |
| // |
| (const raw::EnumMember& element) { |
| linter.CheckCase("enum members", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("enum member", element.identifier); |
| }); |
| |
| callbacks_.OnBitsMember( |
| [& linter = *this, |
| case_check = invalid_case_for_constant, |
| &case_type = upper_snake_] |
| // |
| (const raw::BitsMember& element) { |
| linter.CheckCase("bitfield members", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("bitfield member", element.identifier); |
| }); |
| |
| auto invalid_case_for_decl_name = DefineCheck( |
| "invalid-case-for-decl-name", |
| "${TYPE} must be named in UpperCamelCase"); |
| |
| auto name_repeats_enclosing_type_name = DefineCheck( |
| "name-repeats-enclosing-type-name", |
| "${TYPE} names (${REPEATED_NAMES}) must not repeat names from the " |
| "enclosing ${CONTEXT_TYPE} '${CONTEXT_ID}'"); |
| |
| callbacks_.OnInterfaceDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::InterfaceDeclaration& element) { |
| linter.CheckCase("protocols", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("protocol", element.identifier); |
| linter.EnterContext("protocol", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitInterfaceDeclaration( |
| [& linter = *this] |
| // |
| (const raw::InterfaceDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnMethod( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_] |
| // |
| (const raw::InterfaceMethod& element) { |
| linter.CheckCase("methods", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("method", element.identifier); |
| }); |
| |
| callbacks_.OnEvent( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| event_check = DefineCheck("event-names-must-start-with-on", |
| "Event names must start with 'On'"), |
| &case_type = upper_camel_] |
| // |
| (const raw::InterfaceMethod& element) { |
| std::string id = to_string(element.identifier); |
| auto finding = linter.CheckCase("events", element.identifier, |
| case_check, case_type); |
| if (finding && finding->suggestion().has_value()) { |
| auto& suggestion = finding->suggestion().value(); |
| if (suggestion.replacement().has_value()) { |
| id = suggestion.replacement().value(); |
| } |
| } |
| if ((id.compare(0, 2, "On") != 0) || !isupper(id[2])) { |
| std::string replacement = "On" + id; |
| linter.AddFinding( |
| element.identifier, event_check, |
| { |
| {"IDENTIFIER", id}, |
| {"REPLACEMENT", replacement}, |
| }, |
| "change '${IDENTIFIER}' to '${REPLACEMENT}'", |
| "${REPLACEMENT}"); |
| } |
| linter.CheckRepeatedName("event", element.identifier); |
| }); |
| |
| callbacks_.OnEnumDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::EnumDeclaration& element) { |
| linter.CheckCase("enums", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("enum", element.identifier); |
| linter.EnterContext("enum", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitEnumDeclaration( |
| [& linter = *this] |
| // |
| (const raw::EnumDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnBitsDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::BitsDeclaration& element) { |
| linter.CheckCase("bitfields", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("bitfield", element.identifier); |
| linter.EnterContext("bitfield", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitBitsDeclaration( |
| [& linter = *this] |
| // |
| (const raw::BitsDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnStructDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::StructDeclaration& element) { |
| linter.CheckCase("structs", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("struct", element.identifier); |
| linter.EnterContext("struct", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitStructDeclaration( |
| [& linter = *this] |
| // |
| (const raw::StructDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnTableDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::TableDeclaration& element) { |
| linter.CheckCase("tables", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("table", element.identifier); |
| linter.EnterContext("table", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitTableDeclaration( |
| [& linter = *this] |
| // |
| (const raw::TableDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnUnionDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::UnionDeclaration& element) { |
| linter.CheckCase("unions", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("union", element.identifier); |
| linter.EnterContext("union", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitUnionDeclaration( |
| [& linter = *this] |
| // |
| (const raw::UnionDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnXUnionDeclaration( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_name, |
| &case_type = upper_camel_, |
| context_check = name_repeats_enclosing_type_name] |
| // |
| (const raw::XUnionDeclaration& element) { |
| linter.CheckCase("xunions", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("xunion", element.identifier); |
| linter.EnterContext("xunion", to_string(element.identifier), context_check); |
| }); |
| |
| callbacks_.OnExitXUnionDeclaration( |
| [& linter = *this] |
| // |
| (const raw::XUnionDeclaration& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnFile( |
| [& linter = *this, |
| check = DefineCheck( |
| "disallowed-library-name-component", |
| "Library names must not contain the following components: common, service, util, base, f<letter>l, zx<word>"), |
| context_check = DefineCheck( |
| "name-repeats-library-name", |
| "${TYPE} names (${REPEATED_NAMES}) must not repeat names from the " |
| "library '${CONTEXT_ID}'")] |
| // |
| (const raw::File& element) { |
| static const std::regex disallowed_library_component( |
| R"(^(common|service|util|base|f[a-z]l|zx\w*)$)"); |
| for (const auto& component : element.library_name->components) { |
| if (std::regex_match(to_string(component), |
| disallowed_library_component)) { |
| linter.AddFinding(component, check); |
| break; |
| } |
| } |
| linter.EnterContext("library", to_library_id(element.library_name->components), |
| context_check); |
| }); |
| |
| callbacks_.OnExitFile( |
| [& linter = *this] |
| // |
| (const raw::File& element) { |
| linter.ExitContext(); |
| }); |
| |
| callbacks_.OnFile( |
| [& linter = *this, |
| check = DefineCheck( |
| "wrong-prefix-for-platform-source-library", |
| "FIDL library name is not currently allowed")] |
| // |
| (const raw::File& element) { |
| auto& prefix_component = |
| element.library_name->components.front(); |
| std::string prefix = to_string(prefix_component); |
| if (linter.permitted_library_prefixes_.find(prefix) == |
| linter.permitted_library_prefixes_.end()) { |
| // TODO(fxb/FIDL-547): Implement more specific test, |
| // comparing proposed library prefix to actual |
| // source path. |
| std::string replacement = "fuchsia, perhaps?"; |
| linter.AddFinding( |
| element.library_name, check, |
| { |
| {"ORIGINAL", prefix}, |
| {"REPLACEMENT", replacement}, |
| }, |
| "change '${ORIGINAL}' to ${REPLACEMENT}", |
| "${REPLACEMENT}"); |
| } |
| }); |
| |
| auto invalid_case_for_decl_member = DefineCheck( |
| "invalid-case-for-decl-member", |
| "${TYPE} must be named in lower_snake_case"); |
| |
| callbacks_.OnParameter( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_member, |
| &case_type = lower_snake_] |
| // |
| (const raw::Parameter& element) { |
| linter.CheckCase("parameters", element.identifier, |
| case_check, case_type); |
| }); |
| callbacks_.OnStructMember( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_member, |
| &case_type = lower_snake_] |
| // |
| (const raw::StructMember& element) { |
| linter.CheckCase("struct members", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("struct member", element.identifier); |
| }); |
| callbacks_.OnTableMember( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_member, |
| &case_type = lower_snake_] |
| // |
| (const raw::TableMember& element) { |
| if (element.maybe_used != nullptr) { |
| linter.CheckCase("table members", element.maybe_used->identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("table member", element.maybe_used->identifier); |
| } |
| }); |
| callbacks_.OnUnionMember( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_member, |
| &case_type = lower_snake_] |
| // |
| (const raw::UnionMember& element) { |
| linter.CheckCase("union members", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("union member", element.identifier); |
| }); |
| callbacks_.OnXUnionMember( |
| [& linter = *this, |
| case_check = invalid_case_for_decl_member, |
| &case_type = lower_snake_] |
| // |
| (const raw::XUnionMember& element) { |
| linter.CheckCase("xunion members", element.identifier, |
| case_check, case_type); |
| linter.CheckRepeatedName("xunion member", element.identifier); |
| }); |
| } |
| |
| } // namespace linter |
| } // namespace fidl |