forked from OSchip/llvm-project
Implement code completion for tags, e.g., code completion after "enum"
will provide the names of various enumerations currently visible. Introduced filtering of code-completion results when we build the result set, so that we can identify just the kinds of declarations we want. This implementation is incomplete for C++, since we don't consider that the token after the tag keyword could start a nested-name-specifier. llvm-svn: 82222
This commit is contained in:
parent
bd1af01fcd
commit
f45b0cf389
|
@ -2210,8 +2210,18 @@ public:
|
|||
/// \param IsArrow true when the operator is "->", false when it is ".".
|
||||
virtual void CodeCompleteMemberReferenceExpr(Scope *S, ExprTy *Base,
|
||||
SourceLocation OpLoc,
|
||||
bool IsArrow) {
|
||||
}
|
||||
bool IsArrow) { }
|
||||
|
||||
/// \brief Code completion for a reference to a tag.
|
||||
///
|
||||
/// This code completion action is invoked when the code-completion
|
||||
/// token is found after a tag keyword (struct, union, enum, or class).
|
||||
///
|
||||
/// \para, S the scope in which the tag reference occurs.
|
||||
///
|
||||
/// \param TagSpec an instance of DeclSpec::TST, indicating what kind of tag
|
||||
/// this is (struct/union/enum/class).
|
||||
virtual void CodeCompleteTag(Scope *S, unsigned TagSpec) { }
|
||||
|
||||
/// \brief Code completion for a C++ nested-name-specifier that precedes a
|
||||
/// qualified-id of some form.
|
||||
|
|
|
@ -75,9 +75,17 @@ public:
|
|||
Result(const char *Keyword, unsigned Rank)
|
||||
: Kind(RK_Keyword), Keyword(Keyword), Rank(Rank), Hidden(false) { }
|
||||
};
|
||||
|
||||
|
||||
/// \brief A container of code-completion results.
|
||||
class ResultSet {
|
||||
public:
|
||||
/// \brief The type of a name-lookup filter, which can be provided to the
|
||||
/// name-lookup routines to specify which declarations should be included in
|
||||
/// the result set (when it returns true) and which declarations should be
|
||||
/// filtered out (returns false).
|
||||
typedef bool (CodeCompleteConsumer::*LookupFilter)(NamedDecl *) const;
|
||||
|
||||
private:
|
||||
/// \brief The actual results we have found.
|
||||
std::vector<Result> Results;
|
||||
|
||||
|
@ -87,11 +95,27 @@ public:
|
|||
typedef std::multimap<DeclarationName,
|
||||
std::pair<NamedDecl *, unsigned> > ShadowMap;
|
||||
|
||||
/// \brief The code-completion consumer that is producing these results.
|
||||
CodeCompleteConsumer &Completer;
|
||||
|
||||
/// \brief If non-NULL, a filter function used to remove any code-completion
|
||||
/// results that are not desirable.
|
||||
LookupFilter Filter;
|
||||
|
||||
/// \brief A list of shadow maps, which is used to model name hiding at
|
||||
/// different levels of, e.g., the inheritance hierarchy.
|
||||
std::list<ShadowMap> ShadowMaps;
|
||||
|
||||
|
||||
public:
|
||||
explicit ResultSet(CodeCompleteConsumer &Completer,
|
||||
LookupFilter Filter = 0)
|
||||
: Completer(Completer), Filter(Filter) { }
|
||||
|
||||
/// \brief Set the filter used for code-completion results.
|
||||
void setFilter(LookupFilter Filter) {
|
||||
this->Filter = Filter;
|
||||
}
|
||||
|
||||
typedef std::vector<Result>::iterator iterator;
|
||||
iterator begin() { return Results.begin(); }
|
||||
iterator end() { return Results.end(); }
|
||||
|
@ -99,7 +123,7 @@ public:
|
|||
Result *data() { return Results.empty()? 0 : &Results.front(); }
|
||||
unsigned size() const { return Results.size(); }
|
||||
bool empty() const { return Results.empty(); }
|
||||
|
||||
|
||||
/// \brief Add a new result to this result set (if it isn't already in one
|
||||
/// of the shadow maps), or replace an existing result (for, e.g., a
|
||||
/// redeclaration).
|
||||
|
@ -142,6 +166,10 @@ public:
|
|||
virtual void CodeCompleteMemberReferenceExpr(Scope *S, QualType BaseType,
|
||||
bool IsArrow);
|
||||
|
||||
/// \brief Code completion for a tag name following an enum, class, struct,
|
||||
/// or union keyword.
|
||||
virtual void CodeCompleteTag(Scope *S, ElaboratedType::TagKind TK);
|
||||
|
||||
/// \brief Code completion for a qualified-id, e.g., "std::"
|
||||
///
|
||||
/// \param S the scope in which the nested-name-specifier occurs.
|
||||
|
@ -154,10 +182,27 @@ public:
|
|||
bool EnteringContext);
|
||||
//@}
|
||||
|
||||
/// \name Utility functions
|
||||
/// \name Name lookup functions
|
||||
///
|
||||
/// The name lookup functions in this group collect code-completion results
|
||||
/// by performing a form of name looking into a scope or declaration context.
|
||||
//@{
|
||||
unsigned CollectMemberResults(DeclContext *Ctx, unsigned InitialRank,
|
||||
unsigned CollectLookupResults(Scope *S, unsigned InitialRank,
|
||||
ResultSet &Results);
|
||||
unsigned CollectMemberLookupResults(DeclContext *Ctx, unsigned InitialRank,
|
||||
ResultSet &Results);
|
||||
//@}
|
||||
|
||||
/// \name Name lookup predicates
|
||||
///
|
||||
/// These predicates can be passed to the name lookup functions to filter the
|
||||
/// results of name lookup. All of the predicates have the same type, so that
|
||||
///
|
||||
//@{
|
||||
bool IsNestedNameSpecifier(NamedDecl *ND) const;
|
||||
bool IsEnum(NamedDecl *ND) const;
|
||||
bool IsClassOrStruct(NamedDecl *ND) const;
|
||||
bool IsUnion(NamedDecl *ND) const;
|
||||
//@}
|
||||
};
|
||||
|
||||
|
|
|
@ -1589,7 +1589,12 @@ void Parser::ParseStructUnionBody(SourceLocation RecordLoc,
|
|||
void Parser::ParseEnumSpecifier(SourceLocation StartLoc, DeclSpec &DS,
|
||||
AccessSpecifier AS) {
|
||||
// Parse the tag portion of this.
|
||||
|
||||
if (Tok.is(tok::code_completion)) {
|
||||
// Code completion for an enum name.
|
||||
Actions.CodeCompleteTag(CurScope, DeclSpec::TST_enum);
|
||||
ConsumeToken();
|
||||
}
|
||||
|
||||
AttributeList *Attr = 0;
|
||||
// If attributes exist after tag, parse them.
|
||||
if (Tok.is(tok::kw___attribute))
|
||||
|
|
|
@ -527,6 +527,12 @@ void Parser::ParseClassSpecifier(tok::TokenKind TagTokKind,
|
|||
TagType = DeclSpec::TST_union;
|
||||
}
|
||||
|
||||
if (Tok.is(tok::code_completion)) {
|
||||
// Code completion for a struct, class, or union name.
|
||||
Actions.CodeCompleteTag(CurScope, TagType);
|
||||
ConsumeToken();
|
||||
}
|
||||
|
||||
AttributeList *Attr = 0;
|
||||
// If attributes exist after tag, parse them.
|
||||
if (Tok.is(tok::kw___attribute))
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
#include "clang/Sema/CodeCompleteConsumer.h"
|
||||
#include "clang/Parse/Scope.h"
|
||||
#include "clang/Lex/Preprocessor.h"
|
||||
#include "Sema.h"
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
|
@ -41,11 +42,11 @@ CodeCompleteConsumer::CodeCompleteMemberReferenceExpr(Scope *S,
|
|||
return;
|
||||
}
|
||||
|
||||
ResultSet Results;
|
||||
ResultSet Results(*this);
|
||||
unsigned NextRank = 0;
|
||||
|
||||
if (const RecordType *Record = BaseType->getAs<RecordType>()) {
|
||||
NextRank = CollectMemberResults(Record->getDecl(), NextRank, Results);
|
||||
NextRank = CollectMemberLookupResults(Record->getDecl(), NextRank, Results);
|
||||
|
||||
if (getSema().getLangOptions().CPlusPlus) {
|
||||
if (!Results.empty())
|
||||
|
@ -64,6 +65,32 @@ CodeCompleteConsumer::CodeCompleteMemberReferenceExpr(Scope *S,
|
|||
}
|
||||
}
|
||||
|
||||
void CodeCompleteConsumer::CodeCompleteTag(Scope *S, ElaboratedType::TagKind TK) {
|
||||
ResultSet::LookupFilter Filter = 0;
|
||||
switch (TK) {
|
||||
case ElaboratedType::TK_enum:
|
||||
Filter = &CodeCompleteConsumer::IsEnum;
|
||||
break;
|
||||
|
||||
case ElaboratedType::TK_class:
|
||||
case ElaboratedType::TK_struct:
|
||||
Filter = &CodeCompleteConsumer::IsClassOrStruct;
|
||||
break;
|
||||
|
||||
case ElaboratedType::TK_union:
|
||||
Filter = &CodeCompleteConsumer::IsUnion;
|
||||
break;
|
||||
}
|
||||
|
||||
ResultSet Results(*this, Filter);
|
||||
CollectLookupResults(S, 0, Results);
|
||||
|
||||
// FIXME: In C++, we could have the start of a nested-name-specifier.
|
||||
// Add those results (with a poorer rank, naturally).
|
||||
|
||||
ProcessCodeCompleteResults(Results.data(), Results.size());
|
||||
}
|
||||
|
||||
void
|
||||
CodeCompleteConsumer::CodeCompleteQualifiedId(Scope *S,
|
||||
NestedNameSpecifier *NNS,
|
||||
|
@ -74,8 +101,8 @@ CodeCompleteConsumer::CodeCompleteQualifiedId(Scope *S,
|
|||
if (!Ctx)
|
||||
return;
|
||||
|
||||
ResultSet Results;
|
||||
unsigned NextRank = CollectMemberResults(Ctx, 0, Results);
|
||||
ResultSet Results(*this);
|
||||
unsigned NextRank = CollectMemberLookupResults(Ctx, 0, Results);
|
||||
|
||||
// The "template" keyword can follow "::" in the grammar
|
||||
if (!Results.empty())
|
||||
|
@ -92,6 +119,11 @@ void CodeCompleteConsumer::ResultSet::MaybeAddResult(Result R) {
|
|||
}
|
||||
|
||||
// FIXME: Using declarations
|
||||
// FIXME: Separate overload sets
|
||||
|
||||
// Filter out any unwanted results.
|
||||
if (Filter && !(Completer.*Filter)(R.Declaration))
|
||||
return;
|
||||
|
||||
Decl *CanonDecl = R.Declaration->getCanonicalDecl();
|
||||
unsigned IDNS = CanonDecl->getIdentifierNamespace();
|
||||
|
@ -164,23 +196,89 @@ void CodeCompleteConsumer::ResultSet::ExitScope() {
|
|||
ShadowMaps.pop_back();
|
||||
}
|
||||
|
||||
// Find the next outer declaration context corresponding to this scope.
|
||||
static DeclContext *findOuterContext(Scope *S) {
|
||||
for (S = S->getParent(); S; S = S->getParent())
|
||||
if (S->getEntity())
|
||||
return static_cast<DeclContext *>(S->getEntity())->getPrimaryContext();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// \brief Collect the results of searching for declarations within the given
|
||||
/// scope and its parent scopes.
|
||||
///
|
||||
/// \param S the scope in which we will start looking for declarations.
|
||||
///
|
||||
/// \param InitialRank the initial rank given to results in this scope.
|
||||
/// Larger rank values will be used for results found in parent scopes.
|
||||
unsigned CodeCompleteConsumer::CollectLookupResults(Scope *S,
|
||||
unsigned InitialRank,
|
||||
ResultSet &Results) {
|
||||
if (!S)
|
||||
return InitialRank;
|
||||
|
||||
// FIXME: Using directives!
|
||||
|
||||
unsigned NextRank = InitialRank;
|
||||
Results.EnterNewScope();
|
||||
if (S->getEntity() &&
|
||||
!((DeclContext *)S->getEntity())->isFunctionOrMethod()) {
|
||||
// Look into this scope's declaration context, along with any of its
|
||||
// parent lookup contexts (e.g., enclosing classes), up to the point
|
||||
// where we hit the context stored in the next outer scope.
|
||||
DeclContext *Ctx = (DeclContext *)S->getEntity();
|
||||
DeclContext *OuterCtx = findOuterContext(S);
|
||||
|
||||
for (; Ctx && Ctx->getPrimaryContext() != OuterCtx;
|
||||
Ctx = Ctx->getLookupParent()) {
|
||||
if (Ctx->isFunctionOrMethod())
|
||||
continue;
|
||||
|
||||
NextRank = CollectMemberLookupResults(Ctx, NextRank + 1, Results);
|
||||
}
|
||||
} else if (!S->getParent()) {
|
||||
// Look into the translation unit scope. We walk through the translation
|
||||
// unit's declaration context, because the Scope itself won't have all of
|
||||
// the declarations if
|
||||
NextRank = CollectMemberLookupResults(
|
||||
getSema().Context.getTranslationUnitDecl(),
|
||||
NextRank + 1, Results);
|
||||
} else {
|
||||
// Walk through the declarations in this Scope.
|
||||
for (Scope::decl_iterator D = S->decl_begin(), DEnd = S->decl_end();
|
||||
D != DEnd; ++D) {
|
||||
if (NamedDecl *ND = dyn_cast<NamedDecl>((Decl *)((*D).get())))
|
||||
Results.MaybeAddResult(Result(ND, NextRank));
|
||||
}
|
||||
|
||||
NextRank = NextRank + 1;
|
||||
}
|
||||
|
||||
// Lookup names in the parent scope.
|
||||
NextRank = CollectLookupResults(S->getParent(), NextRank, Results);
|
||||
Results.ExitScope();
|
||||
|
||||
return NextRank;
|
||||
}
|
||||
|
||||
/// \brief Collect the results of searching for members within the given
|
||||
/// declaration context.
|
||||
///
|
||||
/// \param Ctx the declaration context from which we will gather results.
|
||||
///
|
||||
/// \param InitialRank the initial rank given to results in this tag
|
||||
/// declaration. Larger rank values will be used for, e.g., members found
|
||||
/// in base classes.
|
||||
/// \param InitialRank the initial rank given to results in this declaration
|
||||
/// context. Larger rank values will be used for, e.g., members found in
|
||||
/// base classes.
|
||||
///
|
||||
/// \param Results the result set that will be extended with any results
|
||||
/// found within this declaration context (and, for a C++ class, its bases).
|
||||
///
|
||||
/// \returns the next higher rank value, after considering all of the
|
||||
/// names within this declaration context.
|
||||
unsigned CodeCompleteConsumer::CollectMemberResults(DeclContext *Ctx,
|
||||
unsigned InitialRank,
|
||||
ResultSet &Results) {
|
||||
unsigned CodeCompleteConsumer::CollectMemberLookupResults(DeclContext *Ctx,
|
||||
unsigned InitialRank,
|
||||
ResultSet &Results) {
|
||||
// Enumerate all of the results in this context.
|
||||
Results.EnterNewScope();
|
||||
for (DeclContext *CurCtx = Ctx->getPrimaryContext(); CurCtx;
|
||||
|
@ -188,10 +286,8 @@ unsigned CodeCompleteConsumer::CollectMemberResults(DeclContext *Ctx,
|
|||
for (DeclContext::decl_iterator D = CurCtx->decls_begin(),
|
||||
DEnd = CurCtx->decls_end();
|
||||
D != DEnd; ++D) {
|
||||
if (NamedDecl *ND = dyn_cast<NamedDecl>(*D)) {
|
||||
// FIXME: Apply a filter to the results
|
||||
if (NamedDecl *ND = dyn_cast<NamedDecl>(*D))
|
||||
Results.MaybeAddResult(Result(ND, InitialRank));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,9 +331,9 @@ unsigned CodeCompleteConsumer::CollectMemberResults(DeclContext *Ctx,
|
|||
|
||||
// Collect results from this base class (and its bases).
|
||||
NextRank = std::max(NextRank,
|
||||
CollectMemberResults(Record->getDecl(),
|
||||
InitialRank + 1,
|
||||
Results));
|
||||
CollectMemberLookupResults(Record->getDecl(),
|
||||
InitialRank + 1,
|
||||
Results));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,6 +343,46 @@ unsigned CodeCompleteConsumer::CollectMemberResults(DeclContext *Ctx,
|
|||
return NextRank;
|
||||
}
|
||||
|
||||
/// \brief Determines whether the given declaration is suitable as the
|
||||
/// start of a C++ nested-name-specifier, e.g., a class or namespace.
|
||||
bool CodeCompleteConsumer::IsNestedNameSpecifier(NamedDecl *ND) const {
|
||||
// Allow us to find class templates, too.
|
||||
if (ClassTemplateDecl *ClassTemplate = dyn_cast<ClassTemplateDecl>(ND))
|
||||
ND = ClassTemplate->getTemplatedDecl();
|
||||
|
||||
return getSema().isAcceptableNestedNameSpecifier(ND);
|
||||
}
|
||||
|
||||
/// \brief Determines whether the given declaration is an enumeration.
|
||||
bool CodeCompleteConsumer::IsEnum(NamedDecl *ND) const {
|
||||
return isa<EnumDecl>(ND);
|
||||
}
|
||||
|
||||
/// \brief Determines whether the given declaration is a class or struct.
|
||||
bool CodeCompleteConsumer::IsClassOrStruct(NamedDecl *ND) const {
|
||||
// Allow us to find class templates, too.
|
||||
if (ClassTemplateDecl *ClassTemplate = dyn_cast<ClassTemplateDecl>(ND))
|
||||
ND = ClassTemplate->getTemplatedDecl();
|
||||
|
||||
if (RecordDecl *RD = dyn_cast<RecordDecl>(ND))
|
||||
return RD->getTagKind() == TagDecl::TK_class ||
|
||||
RD->getTagKind() == TagDecl::TK_struct;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// \brief Determines whether the given declaration is a union.
|
||||
bool CodeCompleteConsumer::IsUnion(NamedDecl *ND) const {
|
||||
// Allow us to find class templates, too.
|
||||
if (ClassTemplateDecl *ClassTemplate = dyn_cast<ClassTemplateDecl>(ND))
|
||||
ND = ClassTemplate->getTemplatedDecl();
|
||||
|
||||
if (RecordDecl *RD = dyn_cast<RecordDecl>(ND))
|
||||
return RD->getTagKind() == TagDecl::TK_union;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct VISIBILITY_HIDDEN SortCodeCompleteResult {
|
||||
typedef CodeCompleteConsumer::Result Result;
|
||||
|
|
|
@ -2083,6 +2083,7 @@ public:
|
|||
virtual CXXScopeTy *ActOnCXXGlobalScopeSpecifier(Scope *S,
|
||||
SourceLocation CCLoc);
|
||||
|
||||
bool isAcceptableNestedNameSpecifier(NamedDecl *SD);
|
||||
NamedDecl *FindFirstQualifierInScope(Scope *S, NestedNameSpecifier *NNS);
|
||||
|
||||
|
||||
|
@ -3635,6 +3636,8 @@ public:
|
|||
SourceLocation OpLoc,
|
||||
bool IsArrow);
|
||||
|
||||
virtual void CodeCompleteTag(Scope *S, unsigned TagSpec);
|
||||
|
||||
virtual void CodeCompleteQualifiedId(Scope *S, const CXXScopeSpec &SS,
|
||||
bool EnteringContext);
|
||||
//@}
|
||||
|
|
|
@ -260,7 +260,7 @@ Sema::CXXScopeTy *Sema::ActOnCXXGlobalScopeSpecifier(Scope *S,
|
|||
|
||||
/// \brief Determines whether the given declaration is an valid acceptable
|
||||
/// result for name lookup of a nested-name-specifier.
|
||||
bool isAcceptableNestedNameSpecifier(ASTContext &Context, NamedDecl *SD) {
|
||||
bool Sema::isAcceptableNestedNameSpecifier(NamedDecl *SD) {
|
||||
if (!SD)
|
||||
return false;
|
||||
|
||||
|
@ -307,7 +307,7 @@ NamedDecl *Sema::FindFirstQualifierInScope(Scope *S, NestedNameSpecifier *NNS) {
|
|||
assert(!Found.isAmbiguous() && "Cannot handle ambiguities here yet");
|
||||
|
||||
NamedDecl *Result = Found;
|
||||
if (isAcceptableNestedNameSpecifier(Context, Result))
|
||||
if (isAcceptableNestedNameSpecifier(Result))
|
||||
return Result;
|
||||
|
||||
return 0;
|
||||
|
@ -406,7 +406,7 @@ Sema::CXXScopeTy *Sema::BuildCXXNestedNameSpecifier(Scope *S,
|
|||
|
||||
// FIXME: Deal with ambiguities cleanly.
|
||||
NamedDecl *SD = Found;
|
||||
if (isAcceptableNestedNameSpecifier(Context, SD)) {
|
||||
if (isAcceptableNestedNameSpecifier(SD)) {
|
||||
if (!ObjectType.isNull() && !ObjectTypeSearchedInScope) {
|
||||
// C++ [basic.lookup.classref]p4:
|
||||
// [...] If the name is found in both contexts, the
|
||||
|
@ -425,7 +425,7 @@ Sema::CXXScopeTy *Sema::BuildCXXNestedNameSpecifier(Scope *S,
|
|||
|
||||
// FIXME: Handle ambiguities in FoundOuter!
|
||||
NamedDecl *OuterDecl = FoundOuter;
|
||||
if (isAcceptableNestedNameSpecifier(Context, OuterDecl) &&
|
||||
if (isAcceptableNestedNameSpecifier(OuterDecl) &&
|
||||
OuterDecl->getCanonicalDecl() != SD->getCanonicalDecl() &&
|
||||
(!isa<TypeDecl>(OuterDecl) || !isa<TypeDecl>(SD) ||
|
||||
!Context.hasSameType(
|
||||
|
|
|
@ -34,6 +34,35 @@ void Sema::CodeCompleteMemberReferenceExpr(Scope *S, ExprTy *BaseE,
|
|||
CodeCompleter->CodeCompleteMemberReferenceExpr(S, BaseType, IsArrow);
|
||||
}
|
||||
|
||||
void Sema::CodeCompleteTag(Scope *S, unsigned TagSpec) {
|
||||
if (!CodeCompleter)
|
||||
return;
|
||||
|
||||
TagDecl::TagKind TK;
|
||||
switch ((DeclSpec::TST)TagSpec) {
|
||||
case DeclSpec::TST_enum:
|
||||
TK = TagDecl::TK_enum;
|
||||
break;
|
||||
|
||||
case DeclSpec::TST_union:
|
||||
TK = TagDecl::TK_union;
|
||||
break;
|
||||
|
||||
case DeclSpec::TST_struct:
|
||||
TK = TagDecl::TK_struct;
|
||||
break;
|
||||
|
||||
case DeclSpec::TST_class:
|
||||
TK = TagDecl::TK_class;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false && "Unknown type specifier kind in CodeCompleteTag");
|
||||
return;
|
||||
}
|
||||
CodeCompleter->CodeCompleteTag(S, TK);
|
||||
}
|
||||
|
||||
void Sema::CodeCompleteQualifiedId(Scope *S, const CXXScopeSpec &SS,
|
||||
bool EnteringContext) {
|
||||
if (!SS.getScopeRep() || !CodeCompleter)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// RUN: clang-cc -fsyntax-only -code-completion-dump=1 %s -o - | FileCheck -check-prefix=CC1 %s &&
|
||||
// RUN: true
|
||||
|
||||
enum X { x };
|
||||
enum Y { y };
|
||||
struct Z { };
|
||||
|
||||
void X();
|
||||
|
||||
void test() {
|
||||
enum X { x };
|
||||
// CHECK-CC1: X : 0
|
||||
// CHECK-CC1: Y : 2
|
||||
// CHECK-CC1: X : 2 (Hidden)
|
||||
enum
|
Loading…
Reference in New Issue