1 | //===--- StringConstructorCheck.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 "StringConstructorCheck.h" |
10 | #include "../utils/OptionsUtils.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/Tooling/FixIt.h" |
14 | |
15 | using namespace clang::ast_matchers; |
16 | |
17 | namespace clang::tidy::bugprone { |
18 | |
19 | namespace { |
20 | AST_MATCHER_P(IntegerLiteral, isBiggerThan, unsigned, N) { |
21 | return Node.getValue().getZExtValue() > N; |
22 | } |
23 | |
24 | const char DefaultStringNames[] = |
25 | "::std::basic_string;::std::basic_string_view" ; |
26 | |
27 | static std::vector<StringRef> |
28 | removeNamespaces(const std::vector<StringRef> &Names) { |
29 | std::vector<StringRef> Result; |
30 | Result.reserve(n: Names.size()); |
31 | for (StringRef Name : Names) { |
32 | std::string::size_type ColonPos = Name.rfind(C: ':'); |
33 | Result.push_back( |
34 | x: Name.substr(Start: ColonPos == std::string::npos ? 0 : ColonPos + 1)); |
35 | } |
36 | return Result; |
37 | } |
38 | |
39 | } // namespace |
40 | |
41 | StringConstructorCheck::StringConstructorCheck(StringRef Name, |
42 | ClangTidyContext *Context) |
43 | : ClangTidyCheck(Name, Context), |
44 | IsStringviewNullptrCheckEnabled( |
45 | Context->isCheckEnabled(CheckName: "bugprone-stringview-nullptr" )), |
46 | WarnOnLargeLength(Options.get(LocalName: "WarnOnLargeLength" , Default: true)), |
47 | LargeLengthThreshold(Options.get(LocalName: "LargeLengthThreshold" , Default: 0x800000)), |
48 | StringNames(utils::options::parseStringList( |
49 | Option: Options.get(LocalName: "StringNames" , Default: DefaultStringNames))) {} |
50 | |
51 | void StringConstructorCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
52 | Options.store(Options&: Opts, LocalName: "WarnOnLargeLength" , Value: WarnOnLargeLength); |
53 | Options.store(Options&: Opts, LocalName: "LargeLengthThreshold" , Value: LargeLengthThreshold); |
54 | Options.store(Options&: Opts, LocalName: "StringNames" , Value: DefaultStringNames); |
55 | } |
56 | |
57 | void StringConstructorCheck::registerMatchers(MatchFinder *Finder) { |
58 | const auto ZeroExpr = expr(ignoringParenImpCasts(InnerMatcher: integerLiteral(equals(Value: 0)))); |
59 | const auto CharExpr = expr(ignoringParenImpCasts(InnerMatcher: characterLiteral())); |
60 | const auto NegativeExpr = expr(ignoringParenImpCasts( |
61 | InnerMatcher: unaryOperator(hasOperatorName(Name: "-" ), |
62 | hasUnaryOperand(InnerMatcher: integerLiteral(unless(equals(Value: 0))))))); |
63 | const auto LargeLengthExpr = expr(ignoringParenImpCasts( |
64 | InnerMatcher: integerLiteral(isBiggerThan(N: LargeLengthThreshold)))); |
65 | const auto CharPtrType = type(anyOf(pointerType(), arrayType())); |
66 | |
67 | // Match a string-literal; even through a declaration with initializer. |
68 | const auto BoundStringLiteral = stringLiteral().bind(ID: "str" ); |
69 | const auto ConstStrLiteralDecl = varDecl( |
70 | isDefinition(), hasType(InnerMatcher: constantArrayType()), hasType(InnerMatcher: isConstQualified()), |
71 | hasInitializer(InnerMatcher: ignoringParenImpCasts(InnerMatcher: BoundStringLiteral))); |
72 | const auto ConstPtrStrLiteralDecl = varDecl( |
73 | isDefinition(), |
74 | hasType(InnerMatcher: pointerType(pointee(isAnyCharacter(), isConstQualified()))), |
75 | hasInitializer(InnerMatcher: ignoringParenImpCasts(InnerMatcher: BoundStringLiteral))); |
76 | const auto ConstStrLiteral = expr(ignoringParenImpCasts(InnerMatcher: anyOf( |
77 | BoundStringLiteral, declRefExpr(hasDeclaration(InnerMatcher: anyOf( |
78 | ConstPtrStrLiteralDecl, ConstStrLiteralDecl)))))); |
79 | |
80 | // Check the fill constructor. Fills the string with n consecutive copies of |
81 | // character c. [i.e string(size_t n, char c);]. |
82 | Finder->addMatcher( |
83 | NodeMatch: cxxConstructExpr( |
84 | hasDeclaration(InnerMatcher: cxxMethodDecl(hasName(Name: "basic_string" ))), |
85 | hasArgument(N: 0, InnerMatcher: hasType(InnerMatcher: qualType(isInteger()))), |
86 | hasArgument(N: 1, InnerMatcher: hasType(InnerMatcher: qualType(isInteger()))), |
87 | anyOf( |
88 | // Detect the expression: string('x', 40); |
89 | hasArgument(N: 0, InnerMatcher: CharExpr.bind(ID: "swapped-parameter" )), |
90 | // Detect the expression: string(0, ...); |
91 | hasArgument(N: 0, InnerMatcher: ZeroExpr.bind(ID: "empty-string" )), |
92 | // Detect the expression: string(-4, ...); |
93 | hasArgument(N: 0, InnerMatcher: NegativeExpr.bind(ID: "negative-length" )), |
94 | // Detect the expression: string(0x1234567, ...); |
95 | hasArgument(N: 0, InnerMatcher: LargeLengthExpr.bind(ID: "large-length" )))) |
96 | .bind(ID: "constructor" ), |
97 | Action: this); |
98 | |
99 | // Check the literal string constructor with char pointer and length |
100 | // parameters. [i.e. string (const char* s, size_t n);] |
101 | Finder->addMatcher( |
102 | NodeMatch: cxxConstructExpr( |
103 | hasDeclaration(InnerMatcher: cxxConstructorDecl(ofClass( |
104 | InnerMatcher: cxxRecordDecl(hasAnyName(removeNamespaces(Names: StringNames)))))), |
105 | hasArgument(N: 0, InnerMatcher: hasType(InnerMatcher: CharPtrType)), |
106 | hasArgument(N: 1, InnerMatcher: hasType(InnerMatcher: isInteger())), |
107 | anyOf( |
108 | // Detect the expression: string("...", 0); |
109 | hasArgument(N: 1, InnerMatcher: ZeroExpr.bind(ID: "empty-string" )), |
110 | // Detect the expression: string("...", -4); |
111 | hasArgument(N: 1, InnerMatcher: NegativeExpr.bind(ID: "negative-length" )), |
112 | // Detect the expression: string("lit", 0x1234567); |
113 | hasArgument(N: 1, InnerMatcher: LargeLengthExpr.bind(ID: "large-length" )), |
114 | // Detect the expression: string("lit", 5) |
115 | allOf(hasArgument(N: 0, InnerMatcher: ConstStrLiteral.bind(ID: "literal-with-length" )), |
116 | hasArgument(N: 1, InnerMatcher: ignoringParenImpCasts( |
117 | InnerMatcher: integerLiteral().bind(ID: "int" )))))) |
118 | .bind(ID: "constructor" ), |
119 | Action: this); |
120 | |
121 | // Check the literal string constructor with char pointer. |
122 | // [i.e. string (const char* s);] |
123 | Finder->addMatcher( |
124 | NodeMatch: traverse( |
125 | TK: TK_AsIs, |
126 | InnerMatcher: cxxConstructExpr( |
127 | hasDeclaration(InnerMatcher: cxxConstructorDecl(ofClass(InnerMatcher: anyOf( |
128 | cxxRecordDecl(hasName(Name: "basic_string_view" )) |
129 | .bind(ID: "basic_string_view_decl" ), |
130 | cxxRecordDecl(hasAnyName(removeNamespaces(Names: StringNames))))))), |
131 | hasArgument(N: 0, InnerMatcher: expr().bind(ID: "from-ptr" )), |
132 | // do not match std::string(ptr, int) |
133 | // match std::string(ptr, alloc) |
134 | // match std::string(ptr) |
135 | anyOf(hasArgument(N: 1, InnerMatcher: unless(hasType(InnerMatcher: isInteger()))), |
136 | argumentCountIs(N: 1))) |
137 | .bind(ID: "constructor" )), |
138 | Action: this); |
139 | } |
140 | |
141 | void StringConstructorCheck::check(const MatchFinder::MatchResult &Result) { |
142 | const ASTContext &Ctx = *Result.Context; |
143 | const auto *E = Result.Nodes.getNodeAs<CXXConstructExpr>(ID: "constructor" ); |
144 | assert(E && "missing constructor expression" ); |
145 | SourceLocation Loc = E->getBeginLoc(); |
146 | |
147 | if (Result.Nodes.getNodeAs<Expr>(ID: "swapped-parameter" )) { |
148 | const Expr *P0 = E->getArg(Arg: 0); |
149 | const Expr *P1 = E->getArg(Arg: 1); |
150 | diag(Loc, Description: "string constructor parameters are probably swapped;" |
151 | " expecting string(count, character)" ) |
152 | << tooling::fixit::createReplacement(Destination: *P0, Source: *P1, Context: Ctx) |
153 | << tooling::fixit::createReplacement(Destination: *P1, Source: *P0, Context: Ctx); |
154 | } else if (Result.Nodes.getNodeAs<Expr>(ID: "empty-string" )) { |
155 | diag(Loc, Description: "constructor creating an empty string" ); |
156 | } else if (Result.Nodes.getNodeAs<Expr>(ID: "negative-length" )) { |
157 | diag(Loc, Description: "negative value used as length parameter" ); |
158 | } else if (Result.Nodes.getNodeAs<Expr>(ID: "large-length" )) { |
159 | if (WarnOnLargeLength) |
160 | diag(Loc, Description: "suspicious large length parameter" ); |
161 | } else if (Result.Nodes.getNodeAs<Expr>(ID: "literal-with-length" )) { |
162 | const auto *Str = Result.Nodes.getNodeAs<StringLiteral>(ID: "str" ); |
163 | const auto *Lit = Result.Nodes.getNodeAs<IntegerLiteral>(ID: "int" ); |
164 | if (Lit->getValue().ugt(Str->getLength())) { |
165 | diag(Loc, Description: "length is bigger than string literal size" ); |
166 | } |
167 | } else if (const auto *Ptr = Result.Nodes.getNodeAs<Expr>(ID: "from-ptr" )) { |
168 | Expr::EvalResult ConstPtr; |
169 | if (!Ptr->isInstantiationDependent() && |
170 | Ptr->EvaluateAsRValue(Result&: ConstPtr, Ctx) && |
171 | ((ConstPtr.Val.isInt() && ConstPtr.Val.getInt().isZero()) || |
172 | (ConstPtr.Val.isLValue() && ConstPtr.Val.isNullPointer()))) { |
173 | if (IsStringviewNullptrCheckEnabled && |
174 | Result.Nodes.getNodeAs<CXXRecordDecl>(ID: "basic_string_view_decl" )) { |
175 | // Filter out `basic_string_view` to avoid conflicts with |
176 | // `bugprone-stringview-nullptr` |
177 | return; |
178 | } |
179 | diag(Loc, Description: "constructing string from nullptr is undefined behaviour" ); |
180 | } |
181 | } |
182 | } |
183 | |
184 | } // namespace clang::tidy::bugprone |
185 | |