blob: 53e99a1bf74fa313af349718abf4c0f081f8b77b [file] [log] [blame]
// 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