1 | //===--- PropertyDeclarationCheck.cpp - clang-tidy-------------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "PropertyDeclarationCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
12 | #include "llvm/Support/Regex.h" |
13 | |
14 | using namespace clang::ast_matchers; |
15 | |
16 | namespace clang::tidy::objc { |
17 | |
18 | namespace { |
19 | |
20 | // For StandardProperty the naming style is 'lowerCamelCase'. |
21 | // For CategoryProperty especially in categories of system class, |
22 | // to avoid naming conflict, the suggested naming style is |
23 | // 'abc_lowerCamelCase' (adding lowercase prefix followed by '_'). |
24 | // Regardless of the style, all acronyms and initialisms should be capitalized. |
25 | enum NamingStyle { |
26 | StandardProperty = 1, |
27 | CategoryProperty = 2, |
28 | }; |
29 | |
30 | /// For now we will only fix 'CamelCase' or 'abc_CamelCase' property to |
31 | /// 'camelCase' or 'abc_camelCase'. For other cases the users need to |
32 | /// come up with a proper name by their own. |
33 | /// FIXME: provide fix for snake_case to snakeCase |
34 | FixItHint generateFixItHint(const ObjCPropertyDecl *Decl, NamingStyle Style) { |
35 | auto Name = Decl->getName(); |
36 | auto NewName = Decl->getName().str(); |
37 | size_t Index = 0; |
38 | if (Style == CategoryProperty) { |
39 | Index = Name.find_first_of('_') + 1; |
40 | NewName.replace(0, Index - 1, Name.substr(0, Index - 1).lower()); |
41 | } |
42 | if (Index < Name.size()) { |
43 | NewName[Index] = tolower(NewName[Index]); |
44 | if (NewName != Name) { |
45 | return FixItHint::CreateReplacement( |
46 | RemoveRange: CharSourceRange::getTokenRange(R: SourceRange(Decl->getLocation())), |
47 | Code: llvm::StringRef(NewName)); |
48 | } |
49 | } |
50 | return {}; |
51 | } |
52 | |
53 | std::string validPropertyNameRegex(bool UsedInMatcher) { |
54 | // Allow any of these names: |
55 | // foo |
56 | // fooBar |
57 | // url |
58 | // urlString |
59 | // ID |
60 | // IDs |
61 | // URL |
62 | // URLString |
63 | // bundleID |
64 | // CIColor |
65 | // |
66 | // Disallow names of this form: |
67 | // LongString |
68 | // |
69 | // aRbITRaRyCapS is allowed to avoid generating false positives for names |
70 | // like isVitaminBSupplement, CProgrammingLanguage, and isBeforeM. |
71 | std::string StartMatcher = UsedInMatcher ? "::" : "^" ; |
72 | return StartMatcher + "([a-z]|[A-Z][A-Z0-9])[a-z0-9A-Z]*$" ; |
73 | } |
74 | |
75 | bool hasCategoryPropertyPrefix(llvm::StringRef PropertyName) { |
76 | auto RegexExp = |
77 | llvm::Regex("^[a-zA-Z][a-zA-Z0-9]*_[a-zA-Z0-9][a-zA-Z0-9_]+$" ); |
78 | return RegexExp.match(String: PropertyName); |
79 | } |
80 | |
81 | bool prefixedPropertyNameValid(llvm::StringRef PropertyName) { |
82 | size_t Start = PropertyName.find_first_of(C: '_'); |
83 | assert(Start != llvm::StringRef::npos && Start + 1 < PropertyName.size()); |
84 | auto Prefix = PropertyName.substr(Start: 0, N: Start); |
85 | if (Prefix.lower() != Prefix) { |
86 | return false; |
87 | } |
88 | auto RegexExp = llvm::Regex(llvm::StringRef(validPropertyNameRegex(UsedInMatcher: false))); |
89 | return RegexExp.match(String: PropertyName.substr(Start: Start + 1)); |
90 | } |
91 | } // namespace |
92 | |
93 | void PropertyDeclarationCheck::registerMatchers(MatchFinder *Finder) { |
94 | Finder->addMatcher(NodeMatch: objcPropertyDecl( |
95 | // the property name should be in Lower Camel Case like |
96 | // 'lowerCamelCase' |
97 | unless(matchesName(RegExp: validPropertyNameRegex(UsedInMatcher: true)))) |
98 | .bind(ID: "property" ), |
99 | Action: this); |
100 | } |
101 | |
102 | void PropertyDeclarationCheck::check(const MatchFinder::MatchResult &Result) { |
103 | const auto *MatchedDecl = |
104 | Result.Nodes.getNodeAs<ObjCPropertyDecl>(ID: "property" ); |
105 | assert(MatchedDecl->getName().size() > 0); |
106 | auto *DeclContext = MatchedDecl->getDeclContext(); |
107 | auto *CategoryDecl = llvm::dyn_cast<ObjCCategoryDecl>(DeclContext); |
108 | |
109 | if (CategoryDecl != nullptr && |
110 | hasCategoryPropertyPrefix(MatchedDecl->getName())) { |
111 | if (!prefixedPropertyNameValid(MatchedDecl->getName()) || |
112 | CategoryDecl->IsClassExtension()) { |
113 | NamingStyle Style = CategoryDecl->IsClassExtension() ? StandardProperty |
114 | : CategoryProperty; |
115 | diag(MatchedDecl->getLocation(), |
116 | "property name '%0' not using lowerCamelCase style or not prefixed " |
117 | "in a category, according to the Apple Coding Guidelines" ) |
118 | << MatchedDecl->getName() << generateFixItHint(Decl: MatchedDecl, Style); |
119 | } |
120 | return; |
121 | } |
122 | diag(MatchedDecl->getLocation(), |
123 | "property name '%0' not using lowerCamelCase style or not prefixed in " |
124 | "a category, according to the Apple Coding Guidelines" ) |
125 | << MatchedDecl->getName() |
126 | << generateFixItHint(Decl: MatchedDecl, Style: StandardProperty); |
127 | } |
128 | |
129 | } // namespace clang::tidy::objc |
130 | |