[-Wcalled-once-parameter] Introduce 'called_once' attribute

This commit introduces a new attribute `called_once`.
It can be applied to function-like parameters to signify that
this parameter should be called exactly once.  This concept
is particularly widespread in asynchronous programs.

Additionally, this commit introduce a new group of dataflow
analysis-based warnings to check this property.  It identifies
and reports the following situations:
  * parameter is called twice
  * parameter is never called
  * parameter is not called on one of the paths

Current implementation can also automatically infer `called_once`
attribute for completion handler paramaters that should follow the
same principle by convention.  This behavior is OFF by default and
can be turned on by using `-Wcompletion-handler`.

Differential Revision: https://reviews.llvm.org/D92039

rdar://72812043
This commit is contained in:
Valeriy Savchenko 2020-10-21 13:45:28 +03:00
parent 7e4f53f748
commit fec1a442e3
14 changed files with 2921 additions and 7 deletions

View File

@ -51,9 +51,7 @@ public:
return getParentIgnoreParenCasts(const_cast<Stmt*>(S));
}
bool hasParent(Stmt* S) const {
return getParent(S) != nullptr;
}
bool hasParent(const Stmt *S) const { return getParent(S) != nullptr; }
bool isConsumedExpr(Expr *E) const;

View File

@ -0,0 +1,112 @@
//===- CalledOnceCheck.h - Check 'called once' parameters -------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file defines a check for function-like parameters that should be
// called exactly one time.
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_CLANG_ANALYSIS_ANALYSES_CALLEDONCECHECK_H
#define LLVM_CLANG_ANALYSIS_ANALYSES_CALLEDONCECHECK_H
namespace clang {
class AnalysisDeclContext;
class CFG;
class Decl;
class DeclContext;
class Expr;
class ParmVarDecl;
class Stmt;
/// Classification of situations when parameter is not called on every path.
/// \enum IfThen -- then branch of the if statement has no call.
/// \enum IfElse -- else branch of the if statement has no call.
/// \enum Switch -- one of the switch cases doesn't have a call.
/// \enum SwitchSkipped -- there is no call if none of the cases appies.
/// \enum LoopEntered -- no call when the loop is entered.
/// \enum LoopSkipped -- no call when the loop is not entered.
/// \enum FallbackReason -- fallback case when we were not able to figure out
/// the reason.
enum class NeverCalledReason {
IfThen,
IfElse,
Switch,
SwitchSkipped,
LoopEntered,
LoopSkipped,
FallbackReason,
LARGEST_VALUE = FallbackReason
};
class CalledOnceCheckHandler {
public:
CalledOnceCheckHandler() = default;
virtual ~CalledOnceCheckHandler() = default;
/// Called when parameter is called twice.
/// \param Parameter -- parameter that should be called once.
/// \param Call -- call to report the warning.
/// \param PrevCall -- previous call.
/// \param IsCompletionHandler -- true, if parameter is a completion handler.
/// \param Poised -- true, if the second call is guaranteed to happen after
/// the first call.
virtual void handleDoubleCall(const ParmVarDecl *Parameter, const Expr *Call,
const Expr *PrevCall, bool IsCompletionHandler,
bool Poised) {}
/// Called when parameter is not called at all.
/// \param Parameter -- parameter that should be called once.
/// \param IsCompletionHandler -- true, if parameter is a completion handler.
virtual void handleNeverCalled(const ParmVarDecl *Parameter,
bool IsCompletionHandler) {}
/// Called when captured parameter is not called at all.
/// \param Parameter -- parameter that should be called once.
/// \param Where -- declaration that captures \p Parameter
/// \param IsCompletionHandler -- true, if parameter is a completion handler.
virtual void handleCapturedNeverCalled(const ParmVarDecl *Parameter,
const Decl *Where,
bool IsCompletionHandler) {}
/// Called when parameter is not called on one of the paths.
/// Usually we try to find a statement that is the least common ancestor of
/// the path containing the call and not containing the call. This helps us
/// to pinpoint a bad path for the user.
/// \param Parameter -- parameter that should be called once.
/// \param Where -- the least common ancestor statement.
/// \param Reason -- a reason describing the path without a call.
/// \param IsCalledDirectly -- true, if parameter actually gets called on
/// the other path. It is opposed to be used in some other way (added to some
/// collection, passed as a parameter, etc.).
/// \param IsCompletionHandler -- true, if parameter is a completion handler.
virtual void handleNeverCalled(const ParmVarDecl *Parameter,
const Stmt *Where, NeverCalledReason Reason,
bool IsCalledDirectly,
bool IsCompletionHandler) {}
};
/// Check given CFG for 'called once' parameter violations.
///
/// It traverses the function and tracks how such parameters are used.
/// It detects two main violations:
/// * parameter is called twice
/// * parameter is not called
///
/// \param AC -- context.
/// \param Handler -- a handler for found violations.
/// \param CheckConventionalParameters -- true, if we want to check parameters
/// not explicitly marked as 'called once', but having the same requirements
/// according to conventions.
void checkCalledOnceParameters(AnalysisDeclContext &AC,
CalledOnceCheckHandler &Handler,
bool CheckConventionalParameters);
} // end namespace clang
#endif /* LLVM_CLANG_ANALYSIS_ANALYSES_CALLEDONCECHECK_H */

View File

@ -1798,6 +1798,13 @@ def ReturnsNonNull : InheritableAttr {
let Documentation = [ReturnsNonNullDocs];
}
def CalledOnce : Attr {
let Spellings = [Clang<"called_once">];
let Subjects = SubjectList<[ParmVar]>;
let LangOpts = [ObjC];
let Documentation = [CalledOnceDocs];
}
// pass_object_size(N) indicates that the parameter should have
// __builtin_object_size with Type=N evaluated on the parameter at the callsite.
def PassObjectSize : InheritableParamAttr {

View File

@ -5162,6 +5162,59 @@ in the future.
}];
}
def CalledOnceDocs : Documentation {
let Category = DocCatVariable;
let Content = [{
The ``called_once`` attribute specifies that the annotated function or method
parameter is invoked exactly once on all execution paths. It only applies
to parameters with function-like types, i.e. function pointers or blocks. This
concept is particularly useful for asynchronous programs.
Clang implements a check for ``called_once`` parameters,
``-Wcalled-once-parameter``. It is on by default and finds the following
violations:
* Parameter is not called at all.
* Parameter is called more than once.
* Parameter is not called on one of the execution paths.
In the latter case, Clang pinpoints the path where parameter is not invoked
by showing the control-flow statement where the path diverges.
.. code-block:: objc
void fooWithCallback(void (^callback)(void) __attribute__((called_once))) {
if (somePredicate()) {
...
callback();
} esle {
callback(); // OK: callback is called on every path
}
}
void barWithCallback(void (^callback)(void) __attribute__((called_once))) {
if (somePredicate()) {
...
callback(); // note: previous call is here
}
callback(); // warning: callback is called twice
}
void foobarWithCallback(void (^callback)(void) __attribute__((called_once))) {
if (somePredicate()) { // warning: callback is not called when condition is false
...
callback();
}
}
This attribute is useful for API developers who want to double-check if they
implemented their method correctly.
}];
}
def GnuInlineDocs : Documentation {
let Category = DocCatFunction;
let Content = [{

View File

@ -487,6 +487,8 @@ def ObjCPointerIntrospect : DiagGroup<"deprecated-objc-pointer-introspection", [
def ObjCMultipleMethodNames : DiagGroup<"objc-multiple-method-names">;
def ObjCFlexibleArray : DiagGroup<"objc-flexible-array">;
def ObjCBoxing : DiagGroup<"objc-boxing">;
def CompletionHandler : DiagGroup<"completion-handler">;
def CalledOnceParameter : DiagGroup<"called-once-parameter", [CompletionHandler]>;
def OpenCLUnsupportedRGBA: DiagGroup<"opencl-unsupported-rgba">;
def UnderalignedExceptionObject : DiagGroup<"underaligned-exception-object">;
def DeprecatedObjCIsaUsage : DiagGroup<"deprecated-objc-isa-usage">;

View File

@ -3953,6 +3953,40 @@ def err_attribute_preferred_name_arg_invalid : Error<
"argument %0 to 'preferred_name' attribute is not a typedef for "
"a specialization of %1">;
// called-once attribute diagnostics.
def err_called_once_attribute_wrong_type : Error<
"'called_once' attribute only applies to function-like parameters">;
def warn_completion_handler_never_called : Warning<
"%select{|captured }1completion handler is never called">,
InGroup<CompletionHandler>, DefaultIgnore;
def warn_called_once_never_called : Warning<
"%select{|captured }1%0 parameter marked 'called_once' is never called">,
InGroup<CalledOnceParameter>;
def warn_completion_handler_never_called_when : Warning<
"completion handler is never %select{used|called}1 when "
"%select{taking true branch|taking false branch|"
"handling this case|none of the cases applies|"
"entering the loop|skipping the loop|taking one of the branches}2">,
InGroup<CompletionHandler>, DefaultIgnore;
def warn_called_once_never_called_when : Warning<
"%0 parameter marked 'called_once' is never %select{used|called}1 when "
"%select{taking true branch|taking false branch|"
"handling this case|none of the cases applies|"
"entering the loop|skipping the loop|taking one of the branches}2">,
InGroup<CalledOnceParameter>;
def warn_completion_handler_called_twice : Warning<
"completion handler is called twice">,
InGroup<CompletionHandler>, DefaultIgnore;
def warn_called_once_gets_called_twice : Warning<
"%0 parameter marked 'called_once' is called twice">,
InGroup<CalledOnceParameter>;
def note_called_once_gets_called_twice : Note<
"previous call is here%select{; set to nil to indicate "
"it cannot be called afterwards|}0">;
// objc_designated_initializer attribute diagnostics.
def warn_objc_designated_init_missing_super_call : Warning<
"designated initializer missing a 'super' call to a designated initializer of the super class">,

View File

@ -6,6 +6,7 @@ set(LLVM_LINK_COMPONENTS
add_clang_library(clangAnalysis
AnalysisDeclContext.cpp
BodyFarm.cpp
CalledOnceCheck.cpp
CFG.cpp
CFGReachabilityAnalysis.cpp
CFGStmtMap.cpp

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
#include "clang/AST/StmtObjC.h"
#include "clang/AST/StmtVisitor.h"
#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h"
#include "clang/Analysis/Analyses/CalledOnceCheck.h"
#include "clang/Analysis/Analyses/Consumed.h"
#include "clang/Analysis/Analyses/ReachableCode.h"
#include "clang/Analysis/Analyses/ThreadSafety.h"
@ -36,6 +37,7 @@
#include "clang/Lex/Preprocessor.h"
#include "clang/Sema/ScopeInfo.h"
#include "clang/Sema/SemaInternal.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/BitVector.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/SmallString.h"
@ -1623,6 +1625,82 @@ private:
});
}
};
class CalledOnceCheckReporter : public CalledOnceCheckHandler {
public:
CalledOnceCheckReporter(Sema &S) : S(S) {}
void handleDoubleCall(const ParmVarDecl *Parameter, const Expr *Call,
const Expr *PrevCall, bool IsCompletionHandler,
bool Poised) override {
auto DiagToReport = IsCompletionHandler
? diag::warn_completion_handler_called_twice
: diag::warn_called_once_gets_called_twice;
S.Diag(Call->getBeginLoc(), DiagToReport) << Parameter;
S.Diag(PrevCall->getBeginLoc(), diag::note_called_once_gets_called_twice)
<< Poised;
}
void handleNeverCalled(const ParmVarDecl *Parameter,
bool IsCompletionHandler) override {
auto DiagToReport = IsCompletionHandler
? diag::warn_completion_handler_never_called
: diag::warn_called_once_never_called;
S.Diag(Parameter->getBeginLoc(), DiagToReport)
<< Parameter << /* Captured */ false;
}
void handleNeverCalled(const ParmVarDecl *Parameter, const Stmt *Where,
NeverCalledReason Reason, bool IsCalledDirectly,
bool IsCompletionHandler) override {
auto DiagToReport = IsCompletionHandler
? diag::warn_completion_handler_never_called_when
: diag::warn_called_once_never_called_when;
S.Diag(Where->getBeginLoc(), DiagToReport)
<< Parameter << IsCalledDirectly << (unsigned)Reason;
}
void handleCapturedNeverCalled(const ParmVarDecl *Parameter,
const Decl *Where,
bool IsCompletionHandler) override {
auto DiagToReport = IsCompletionHandler
? diag::warn_completion_handler_never_called
: diag::warn_called_once_never_called;
S.Diag(Where->getBeginLoc(), DiagToReport)
<< Parameter << /* Captured */ true;
}
private:
Sema &S;
};
constexpr unsigned CalledOnceWarnings[] = {
diag::warn_called_once_never_called,
diag::warn_called_once_never_called_when,
diag::warn_called_once_gets_called_twice};
constexpr unsigned CompletionHandlerWarnings[]{
diag::warn_completion_handler_never_called,
diag::warn_completion_handler_never_called_when,
diag::warn_completion_handler_called_twice};
bool shouldAnalyzeCalledOnceImpl(llvm::ArrayRef<unsigned> DiagIDs,
const DiagnosticsEngine &Diags,
SourceLocation At) {
return llvm::any_of(DiagIDs, [&Diags, At](unsigned DiagID) {
return !Diags.isIgnored(DiagID, At);
});
}
bool shouldAnalyzeCalledOnceConventions(const DiagnosticsEngine &Diags,
SourceLocation At) {
return shouldAnalyzeCalledOnceImpl(CompletionHandlerWarnings, Diags, At);
}
bool shouldAnalyzeCalledOnceParameters(const DiagnosticsEngine &Diags,
SourceLocation At) {
return shouldAnalyzeCalledOnceImpl(CalledOnceWarnings, Diags, At) ||
shouldAnalyzeCalledOnceConventions(Diags, At);
}
} // anonymous namespace
namespace clang {
@ -2264,6 +2342,17 @@ AnalysisBasedWarnings::IssueWarnings(sema::AnalysisBasedWarnings::Policy P,
}
}
// Check for violations of "called once" parameter properties.
if (S.getLangOpts().ObjC &&
shouldAnalyzeCalledOnceParameters(Diags, D->getBeginLoc())) {
if (AC.getCFG()) {
CalledOnceCheckReporter Reporter(S);
checkCalledOnceParameters(
AC, Reporter,
shouldAnalyzeCalledOnceConventions(Diags, D->getBeginLoc()));
}
}
bool FallThroughDiagFull =
!Diags.isIgnored(diag::warn_unannotated_fallthrough, D->getBeginLoc());
bool FallThroughDiagPerFunction = !Diags.isIgnored(

View File

@ -3693,6 +3693,26 @@ static void handleCallbackAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
S.Context, AL, EncodingIndices.data(), EncodingIndices.size()));
}
static bool isFunctionLike(const Type &T) {
// Check for explicit function types.
// 'called_once' is only supported in Objective-C and it has
// function pointers and block pointers.
return T.isFunctionPointerType() || T.isBlockPointerType();
}
/// Handle 'called_once' attribute.
static void handleCalledOnceAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
// 'called_once' only applies to parameters representing functions.
QualType T = cast<ParmVarDecl>(D)->getType();
if (!isFunctionLike(*T)) {
S.Diag(AL.getLoc(), diag::err_called_once_attribute_wrong_type);
return;
}
D->addAttr(::new (S.Context) CalledOnceAttr(S.Context, AL));
}
static void handleTransparentUnionAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
// Try to find the underlying union declaration.
RecordDecl *RD = nullptr;
@ -7692,6 +7712,9 @@ static void ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D,
case ParsedAttr::AT_Callback:
handleCallbackAttr(S, D, AL);
break;
case ParsedAttr::AT_CalledOnce:
handleCalledOnceAttr(S, D, AL);
break;
case ParsedAttr::AT_CUDAGlobal:
handleGlobalAttr(S, D, AL);
break;

View File

@ -15404,10 +15404,6 @@ ExprResult Sema::ActOnBlockStmtExpr(SourceLocation CaretLoc,
PopDeclContext();
// Pop the block scope now but keep it alive to the end of this function.
AnalysisBasedWarnings::Policy WP = AnalysisWarnings.getDefaultPolicy();
PoppedFunctionScopePtr ScopeRAII = PopFunctionScopeInfo(&WP, BD, BlockTy);
// Set the captured variables on the block.
SmallVector<BlockDecl::Capture, 4> Captures;
for (Capture &Cap : BSI->Captures) {
@ -15475,6 +15471,10 @@ ExprResult Sema::ActOnBlockStmtExpr(SourceLocation CaretLoc,
}
BD->setCaptures(Context, Captures, BSI->CXXThisCaptureIndex != 0);
// Pop the block scope now but keep it alive to the end of this function.
AnalysisBasedWarnings::Policy WP = AnalysisWarnings.getDefaultPolicy();
PoppedFunctionScopePtr ScopeRAII = PopFunctionScopeInfo(&WP, BD, BlockTy);
BlockExpr *Result = new (Context) BlockExpr(BD, BlockTy);
// If the block isn't obviously global, i.e. it captures anything at

View File

@ -40,6 +40,7 @@
// CHECK-NEXT: CXX11NoReturn (SubjectMatchRule_function)
// CHECK-NEXT: CallableWhen (SubjectMatchRule_function_is_member)
// CHECK-NEXT: Callback (SubjectMatchRule_function)
// CHECK-NEXT: CalledOnce (SubjectMatchRule_variable_is_parameter)
// CHECK-NEXT: Capability (SubjectMatchRule_record, SubjectMatchRule_type_alias)
// CHECK-NEXT: CarriesDependency (SubjectMatchRule_variable_is_parameter, SubjectMatchRule_objc_method, SubjectMatchRule_function)
// CHECK-NEXT: Cleanup (SubjectMatchRule_variable_is_local)

View File

@ -0,0 +1,20 @@
// RUN: %clang_cc1 -verify -fsyntax-only -fobjc-arc -fblocks %s
#define CALLED_ONCE __attribute__((called_once))
void test1(int x CALLED_ONCE); // expected-error{{'called_once' attribute only applies to function-like parameters}}
void test2(double x CALLED_ONCE); // expected-error{{'called_once' attribute only applies to function-like parameters}}
void test3(void (*foo)() CALLED_ONCE); // no-error
void test4(int (^foo)(int) CALLED_ONCE); // no-error
void test5(void (*foo)() __attribute__((called_once(1))));
// expected-error@-1{{'called_once' attribute takes no arguments}}
void test6(void (*foo)() __attribute__((called_once("str1", "str2"))));
// expected-error@-1{{'called_once' attribute takes no arguments}}
CALLED_ONCE void test7(); // expected-warning{{'called_once' attribute only applies to parameters}}
void test8() {
void (*foo)() CALLED_ONCE; // expected-warning{{'called_once' attribute only applies to parameters}}
foo();
}

File diff suppressed because it is too large Load Diff