blob: d7715fa2c695ca1b9401b02c39475fc8cd14675a [file] [log] [blame]
//===--- PropertyDeclarationCheck.cpp - clang-tidy-------------------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
#include "PropertyDeclarationCheck.h"
#include <algorithm>
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Basic/CharInfo.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Regex.h"
using namespace clang::ast_matchers;
namespace clang {
namespace tidy {
namespace objc {
namespace {
// For StandardProperty the naming style is 'lowerCamelCase'.
// For CategoryProperty especially in categories of system class,
// to avoid naming conflict, the suggested naming style is
// 'abc_lowerCamelCase' (adding lowercase prefix followed by '_').
enum NamingStyle {
StandardProperty = 1,
CategoryProperty = 2,
};
/// The acronyms are aggregated from multiple sources including
/// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/APIAbbreviations.html#//apple_ref/doc/uid/20001285-BCIHCGAE
///
/// Keep this list sorted.
constexpr llvm::StringLiteral DefaultSpecialAcronyms[] = {
"[2-9]G",
"ACL",
"API",
"AR",
"ARGB",
"ASCII",
"AV",
"BGRA",
"CA",
"CF",
"CG",
"CI",
"CRC",
"CV",
"CMYK",
"DNS",
"FPS",
"FTP",
"GIF",
"GL",
"GPS",
"GUID",
"HD",
"HDR",
"HMAC",
"HTML",
"HTTP",
"HTTPS",
"HUD",
"ID",
"JPG",
"JS",
"LAN",
"LZW",
"MAC",
"MD",
"MDNS",
"MIDI",
"NS",
"OS",
"PDF",
"PIN",
"PNG",
"POI",
"PSTN",
"PTR",
"QA",
"QOS",
"RGB",
"RGBA",
"RGBX",
"RIPEMD",
"ROM",
"RPC",
"RTF",
"RTL",
"SC",
"SDK",
"SHA",
"SQL",
"SSO",
"TCP",
"TIFF",
"TTS",
"UI",
"URI",
"URL",
"UUID",
"VC",
"VOIP",
"VPN",
"VR",
"W",
"WAN",
"X",
"XML",
"Y",
"Z",
};
/// For now we will only fix 'CamelCase' or 'abc_CamelCase' property to
/// 'camelCase' or 'abc_camelCase'. For other cases the users need to
/// come up with a proper name by their own.
/// FIXME: provide fix for snake_case to snakeCase
FixItHint generateFixItHint(const ObjCPropertyDecl *Decl, NamingStyle Style) {
auto Name = Decl->getName();
auto NewName = Decl->getName().str();
size_t Index = 0;
if (Style == CategoryProperty) {
Index = Name.find_first_of('_') + 1;
NewName.replace(0, Index - 1, Name.substr(0, Index - 1).lower());
}
if (Index < Name.size()) {
NewName[Index] = tolower(NewName[Index]);
if (NewName != Name) {
return FixItHint::CreateReplacement(
CharSourceRange::getTokenRange(SourceRange(Decl->getLocation())),
llvm::StringRef(NewName));
}
}
return FixItHint();
}
std::string AcronymsGroupRegex(llvm::ArrayRef<std::string> EscapedAcronyms) {
return "(" +
llvm::join(EscapedAcronyms.begin(), EscapedAcronyms.end(), "s?|") +
"s?)";
}
std::string validPropertyNameRegex(llvm::ArrayRef<std::string> EscapedAcronyms,
bool UsedInMatcher) {
// Allow any of these names:
// foo
// fooBar
// url
// urlString
// URL
// URLString
// bundleID
std::string StartMatcher = UsedInMatcher ? "::" : "^";
std::string AcronymsMatcher = AcronymsGroupRegex(EscapedAcronyms);
return StartMatcher + "(" + AcronymsMatcher + "[A-Z]?)?[a-z]+[a-z0-9]*(" +
AcronymsMatcher + "|([A-Z][a-z0-9]+)|A|I)*$";
}
bool hasCategoryPropertyPrefix(llvm::StringRef PropertyName) {
auto RegexExp = llvm::Regex("^[a-zA-Z]+_[a-zA-Z0-9][a-zA-Z0-9_]+$");
return RegexExp.match(PropertyName);
}
bool prefixedPropertyNameValid(llvm::StringRef PropertyName,
llvm::ArrayRef<std::string> Acronyms) {
size_t Start = PropertyName.find_first_of('_');
assert(Start != llvm::StringRef::npos && Start + 1 < PropertyName.size());
auto Prefix = PropertyName.substr(0, Start);
if (Prefix.lower() != Prefix) {
return false;
}
auto RegexExp =
llvm::Regex(llvm::StringRef(validPropertyNameRegex(Acronyms, false)));
return RegexExp.match(PropertyName.substr(Start + 1));
}
} // namespace
PropertyDeclarationCheck::PropertyDeclarationCheck(StringRef Name,
ClangTidyContext *Context)
: ClangTidyCheck(Name, Context),
SpecialAcronyms(
utils::options::parseStringList(Options.get("Acronyms", ""))),
IncludeDefaultAcronyms(Options.get("IncludeDefaultAcronyms", true)),
EscapedAcronyms() {}
void PropertyDeclarationCheck::registerMatchers(MatchFinder *Finder) {
// this check should only be applied to ObjC sources.
if (!getLangOpts().ObjC1 && !getLangOpts().ObjC2) {
return;
}
if (IncludeDefaultAcronyms) {
EscapedAcronyms.reserve(llvm::array_lengthof(DefaultSpecialAcronyms) +
SpecialAcronyms.size());
// No need to regex-escape the default acronyms.
EscapedAcronyms.insert(EscapedAcronyms.end(),
std::begin(DefaultSpecialAcronyms),
std::end(DefaultSpecialAcronyms));
} else {
EscapedAcronyms.reserve(SpecialAcronyms.size());
}
// In case someone defines a prefix which includes a regex
// special character, regex-escape all the user-defined prefixes.
std::transform(SpecialAcronyms.begin(), SpecialAcronyms.end(),
std::back_inserter(EscapedAcronyms),
[](const std::string &s) { return llvm::Regex::escape(s); });
Finder->addMatcher(
objcPropertyDecl(
// the property name should be in Lower Camel Case like
// 'lowerCamelCase'
unless(matchesName(validPropertyNameRegex(EscapedAcronyms, true))))
.bind("property"),
this);
}
void PropertyDeclarationCheck::check(const MatchFinder::MatchResult &Result) {
const auto *MatchedDecl =
Result.Nodes.getNodeAs<ObjCPropertyDecl>("property");
assert(MatchedDecl->getName().size() > 0);
auto *DeclContext = MatchedDecl->getDeclContext();
auto *CategoryDecl = llvm::dyn_cast<ObjCCategoryDecl>(DeclContext);
auto AcronymsRegex =
llvm::Regex("^" + AcronymsGroupRegex(EscapedAcronyms) + "$");
if (AcronymsRegex.match(MatchedDecl->getName())) {
return;
}
if (CategoryDecl != nullptr &&
hasCategoryPropertyPrefix(MatchedDecl->getName())) {
if (!prefixedPropertyNameValid(MatchedDecl->getName(), EscapedAcronyms) ||
CategoryDecl->IsClassExtension()) {
NamingStyle Style = CategoryDecl->IsClassExtension() ? StandardProperty
: CategoryProperty;
diag(MatchedDecl->getLocation(),
"property name '%0' not using lowerCamelCase style or not prefixed "
"in a category, according to the Apple Coding Guidelines")
<< MatchedDecl->getName() << generateFixItHint(MatchedDecl, Style);
}
return;
}
diag(MatchedDecl->getLocation(),
"property name '%0' not using lowerCamelCase style or not prefixed in "
"a category, according to the Apple Coding Guidelines")
<< MatchedDecl->getName()
<< generateFixItHint(MatchedDecl, StandardProperty);
}
void PropertyDeclarationCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, "Acronyms",
utils::options::serializeStringList(SpecialAcronyms));
Options.store(Opts, "IncludeDefaultAcronyms", IncludeDefaultAcronyms);
}
} // namespace objc
} // namespace tidy
} // namespace clang