1 //===--- PropertyDeclarationCheck.cpp - clang-tidy-------------------------===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is distributed under the University of Illinois Open Source
6 // License. See LICENSE.TXT for details.
7 //
8 //===----------------------------------------------------------------------===//
9 
10 #include "PropertyDeclarationCheck.h"
11 #include <algorithm>
12 #include "../utils/OptionsUtils.h"
13 #include "clang/AST/ASTContext.h"
14 #include "clang/ASTMatchers/ASTMatchFinder.h"
15 #include "clang/Basic/CharInfo.h"
16 #include "llvm/ADT/STLExtras.h"
17 #include "llvm/ADT/StringExtras.h"
18 #include "llvm/Support/Regex.h"
19 
20 using namespace clang::ast_matchers;
21 
22 namespace clang {
23 namespace tidy {
24 namespace objc {
25 
26 namespace {
27 
28 // For StandardProperty the naming style is 'lowerCamelCase'.
29 // For CategoryProperty especially in categories of system class,
30 // to avoid naming conflict, the suggested naming style is
31 // 'abc_lowerCamelCase' (adding lowercase prefix followed by '_').
32 enum NamingStyle {
33   StandardProperty = 1,
34   CategoryProperty = 2,
35 };
36 
37 /// The acronyms are from
38 /// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/APIAbbreviations.html#//apple_ref/doc/uid/20001285-BCIHCGAE
39 ///
40 /// Keep this list sorted.
41 constexpr llvm::StringLiteral DefaultSpecialAcronyms[] = {
42     "ACL",
43     "API",
44     "ARGB",
45     "ASCII",
46     "BGRA",
47     "CMYK",
48     "DNS",
49     "FPS",
50     "FTP",
51     "GIF",
52     "GPS",
53     "HD",
54     "HDR",
55     "HTML",
56     "HTTP",
57     "HTTPS",
58     "HUD",
59     "ID",
60     "JPG",
61     "JS",
62     "LAN",
63     "LZW",
64     "MDNS",
65     "MIDI",
66     "OS",
67     "PDF",
68     "PIN",
69     "PNG",
70     "POI",
71     "PSTN",
72     "PTR",
73     "QA",
74     "QOS",
75     "RGB",
76     "RGBA",
77     "RGBX",
78     "ROM",
79     "RPC",
80     "RTF",
81     "RTL",
82     "SDK",
83     "SSO",
84     "TCP",
85     "TIFF",
86     "TTS",
87     "UI",
88     "URI",
89     "URL",
90     "VC",
91     "VOIP",
92     "VPN",
93     "VR",
94     "WAN",
95     "XML",
96 };
97 
98 /// For now we will only fix 'CamelCase' or 'abc_CamelCase' property to
99 /// 'camelCase' or 'abc_camelCase'. For other cases the users need to
100 /// come up with a proper name by their own.
101 /// FIXME: provide fix for snake_case to snakeCase
102 FixItHint generateFixItHint(const ObjCPropertyDecl *Decl, NamingStyle Style) {
103   auto Name = Decl->getName();
104   auto NewName = Decl->getName().str();
105   size_t Index = 0;
106   if (Style == CategoryProperty) {
107     Index = Name.find_first_of('_') + 1;
108     NewName.replace(0, Index - 1, Name.substr(0, Index - 1).lower());
109   }
110   if (Index < Name.size()) {
111     NewName[Index] = tolower(NewName[Index]);
112     if (NewName != Name) {
113       return FixItHint::CreateReplacement(
114           CharSourceRange::getTokenRange(SourceRange(Decl->getLocation())),
115           llvm::StringRef(NewName));
116     }
117   }
118   return FixItHint();
119 }
120 
121 std::string AcronymsGroupRegex(llvm::ArrayRef<std::string> EscapedAcronyms) {
122   return "(" +
123          llvm::join(EscapedAcronyms.begin(), EscapedAcronyms.end(), "s?|") +
124          "s?)";
125 }
126 
127 std::string validPropertyNameRegex(llvm::ArrayRef<std::string> EscapedAcronyms,
128                                    bool UsedInMatcher) {
129   // Allow any of these names:
130   // foo
131   // fooBar
132   // url
133   // urlString
134   // URL
135   // URLString
136   // bundleID
137   std::string StartMatcher = UsedInMatcher ? "::" : "^";
138   std::string AcronymsMatcher = AcronymsGroupRegex(EscapedAcronyms);
139   return StartMatcher + "(" + AcronymsMatcher + "[A-Z]?)?[a-z]+[a-z0-9]*(" +
140          AcronymsMatcher + "|([A-Z][a-z0-9]+))*$";
141 }
142 
143 bool hasCategoryPropertyPrefix(llvm::StringRef PropertyName) {
144   auto RegexExp = llvm::Regex("^[a-zA-Z]+_[a-zA-Z0-9][a-zA-Z0-9_]+$");
145   return RegexExp.match(PropertyName);
146 }
147 
148 bool prefixedPropertyNameValid(llvm::StringRef PropertyName,
149                                llvm::ArrayRef<std::string> Acronyms) {
150   size_t Start = PropertyName.find_first_of('_');
151   assert(Start != llvm::StringRef::npos && Start + 1 < PropertyName.size());
152   auto Prefix = PropertyName.substr(0, Start);
153   if (Prefix.lower() != Prefix) {
154     return false;
155   }
156   auto RegexExp =
157       llvm::Regex(llvm::StringRef(validPropertyNameRegex(Acronyms, false)));
158   return RegexExp.match(PropertyName.substr(Start + 1));
159 }
160 }  // namespace
161 
162 PropertyDeclarationCheck::PropertyDeclarationCheck(StringRef Name,
163                                                    ClangTidyContext *Context)
164     : ClangTidyCheck(Name, Context),
165       SpecialAcronyms(
166           utils::options::parseStringList(Options.get("Acronyms", ""))),
167       IncludeDefaultAcronyms(Options.get("IncludeDefaultAcronyms", true)),
168       EscapedAcronyms() {}
169 
170 void PropertyDeclarationCheck::registerMatchers(MatchFinder *Finder) {
171   if (IncludeDefaultAcronyms) {
172     EscapedAcronyms.reserve(llvm::array_lengthof(DefaultSpecialAcronyms) +
173                             SpecialAcronyms.size());
174     // No need to regex-escape the default acronyms.
175     EscapedAcronyms.insert(EscapedAcronyms.end(),
176                            std::begin(DefaultSpecialAcronyms),
177                            std::end(DefaultSpecialAcronyms));
178   } else {
179     EscapedAcronyms.reserve(SpecialAcronyms.size());
180   }
181   // In case someone defines a prefix which includes a regex
182   // special character, regex-escape all the user-defined prefixes.
183   std::transform(SpecialAcronyms.begin(), SpecialAcronyms.end(),
184                  std::back_inserter(EscapedAcronyms),
185                  [](const std::string &s) { return llvm::Regex::escape(s); });
186   Finder->addMatcher(
187       objcPropertyDecl(
188           // the property name should be in Lower Camel Case like
189           // 'lowerCamelCase'
190           unless(matchesName(validPropertyNameRegex(EscapedAcronyms, true))))
191           .bind("property"),
192       this);
193 }
194 
195 void PropertyDeclarationCheck::check(const MatchFinder::MatchResult &Result) {
196   const auto *MatchedDecl =
197       Result.Nodes.getNodeAs<ObjCPropertyDecl>("property");
198   assert(MatchedDecl->getName().size() > 0);
199   auto *DeclContext = MatchedDecl->getDeclContext();
200   auto *CategoryDecl = llvm::dyn_cast<ObjCCategoryDecl>(DeclContext);
201   if (CategoryDecl != nullptr &&
202       hasCategoryPropertyPrefix(MatchedDecl->getName())) {
203     if (!prefixedPropertyNameValid(MatchedDecl->getName(), EscapedAcronyms) ||
204         CategoryDecl->IsClassExtension()) {
205       NamingStyle Style = CategoryDecl->IsClassExtension() ? StandardProperty
206                                                            : CategoryProperty;
207       diag(MatchedDecl->getLocation(),
208            "property name '%0' not using lowerCamelCase style or not prefixed "
209            "in a category, according to the Apple Coding Guidelines")
210           << MatchedDecl->getName() << generateFixItHint(MatchedDecl, Style);
211     }
212     return;
213   }
214   diag(MatchedDecl->getLocation(),
215        "property name '%0' not using lowerCamelCase style or not prefixed in "
216        "a category, according to the Apple Coding Guidelines")
217       << MatchedDecl->getName()
218       << generateFixItHint(MatchedDecl, StandardProperty);
219 }
220 
221 void PropertyDeclarationCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
222   Options.store(Opts, "Acronyms",
223                 utils::options::serializeStringList(SpecialAcronyms));
224   Options.store(Opts, "IncludeDefaultAcronyms", IncludeDefaultAcronyms);
225 }
226 
227 }  // namespace objc
228 }  // namespace tidy
229 }  // namespace clang
230