diff options
author | Douglas Gregor <dgregor@apple.com> | 2010-01-03 18:01:57 +0000 |
---|---|---|
committer | Douglas Gregor <dgregor@apple.com> | 2010-01-03 18:01:57 +0000 |
commit | f06cdae9c68dfc4191fbf6b9e5ea0fd748488d88 (patch) | |
tree | 2354bfa622035a1dd2cf4d18161aee9acd92e1f0 | |
parent | 368a55d3ce5d66c6d0502c6f8bf061c06961042c (diff) |
Implement typo correction for a variety of Objective-C-specific
constructs:
- Instance variable lookup ("foo->ivar" and, in instance methods, "ivar")
- Property name lookup ("foo.prop")
- Superclasses
- Various places where a class name is required
- Protocol names (e.g., id<proto>)
This seems to cover many of the common places where typos could occur.
git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@92449 91177308-0d34-0410-b5e6-96231b3b80d8
-rw-r--r-- | include/clang/Basic/DiagnosticSemaKinds.td | 11 | ||||
-rw-r--r-- | lib/Sema/Sema.h | 6 | ||||
-rw-r--r-- | lib/Sema/SemaDecl.cpp | 26 | ||||
-rw-r--r-- | lib/Sema/SemaDeclObjC.cpp | 26 | ||||
-rw-r--r-- | lib/Sema/SemaExpr.cpp | 38 | ||||
-rw-r--r-- | lib/Sema/SemaExprObjC.cpp | 5 | ||||
-rw-r--r-- | lib/Sema/SemaLookup.cpp | 95 | ||||
-rw-r--r-- | test/FixIt/typo.m | 80 | ||||
-rw-r--r-- | test/SemaObjC/category-1.m | 2 | ||||
-rw-r--r-- | test/SemaObjC/undef-class-messagin-error.m | 4 |
10 files changed, 277 insertions, 16 deletions
diff --git a/include/clang/Basic/DiagnosticSemaKinds.td b/include/clang/Basic/DiagnosticSemaKinds.td index 155633b08f..fb7a6abf10 100644 --- a/include/clang/Basic/DiagnosticSemaKinds.td +++ b/include/clang/Basic/DiagnosticSemaKinds.td @@ -2569,6 +2569,17 @@ def err_mem_init_not_member_or_class_suggest : Error< def err_field_designator_unknown_suggest : Error< "field designator %0 does not refer to any field in type %1; did you mean " "%2?">; +def err_typecheck_member_reference_ivar_suggest : Error< + "%0 does not have a member named %1; did you mean %2?">; +def err_property_not_found_suggest : Error< + "property %0 not found on object of type %1; did you mean %2?">; +def err_undef_interface_suggest : Error< + "cannot find interface declaration for %0; did you mean %1?">; +def err_undef_superclass_suggest : Error< + "cannot find interface declaration for %0, superclass of %1; did you mean " + "%2?">; +def err_undeclared_protocol_suggest : Error< + "cannot find protocol declaration for %0; did you mean %1?">; } diff --git a/lib/Sema/Sema.h b/lib/Sema/Sema.h index 54e6f4a0cf..110a626835 100644 --- a/lib/Sema/Sema.h +++ b/lib/Sema/Sema.h @@ -1208,7 +1208,8 @@ public: bool CorrectTypo(LookupResult &R, Scope *S, const CXXScopeSpec *SS, DeclContext *MemberContext = 0, - bool EnteringContext = false); + bool EnteringContext = false, + const ObjCObjectPointerType *OPT = 0); void FindAssociatedClassesAndNamespaces(Expr **Args, unsigned NumArgs, AssociatedNamespaceSet &AssociatedNamespaces, @@ -1217,7 +1218,8 @@ public: bool DiagnoseAmbiguousLookup(LookupResult &Result); //@} - ObjCInterfaceDecl *getObjCInterfaceDecl(IdentifierInfo *Id); + ObjCInterfaceDecl *getObjCInterfaceDecl(IdentifierInfo *&Id, + SourceLocation RecoverLoc = SourceLocation()); NamedDecl *LazilyCreateBuiltin(IdentifierInfo *II, unsigned ID, Scope *S, bool ForRedeclaration, SourceLocation Loc); diff --git a/lib/Sema/SemaDecl.cpp b/lib/Sema/SemaDecl.cpp index 2253f093df..30f9040000 100644 --- a/lib/Sema/SemaDecl.cpp +++ b/lib/Sema/SemaDecl.cpp @@ -555,11 +555,35 @@ void Sema::ActOnPopScope(SourceLocation Loc, Scope *S) { /// getObjCInterfaceDecl - Look up a for a class declaration in the scope. /// return 0 if one not found. -ObjCInterfaceDecl *Sema::getObjCInterfaceDecl(IdentifierInfo *Id) { +/// +/// \param Id the name of the Objective-C class we're looking for. If +/// typo-correction fixes this name, the Id will be updated +/// to the fixed name. +/// +/// \param RecoverLoc if provided, this routine will attempt to +/// recover from a typo in the name of an existing Objective-C class +/// and, if successful, will return the lookup that results from +/// typo-correction. +ObjCInterfaceDecl *Sema::getObjCInterfaceDecl(IdentifierInfo *&Id, + SourceLocation RecoverLoc) { // The third "scope" argument is 0 since we aren't enabling lazy built-in // creation from this context. NamedDecl *IDecl = LookupSingleName(TUScope, Id, LookupOrdinaryName); + if (!IDecl && !RecoverLoc.isInvalid()) { + // Perform typo correction at the given location, but only if we + // find an Objective-C class name. + LookupResult R(*this, Id, RecoverLoc, LookupOrdinaryName); + if (CorrectTypo(R, TUScope, 0) && + (IDecl = R.getAsSingle<ObjCInterfaceDecl>())) { + Diag(RecoverLoc, diag::err_undef_interface_suggest) + << Id << IDecl->getDeclName() + << CodeModificationHint::CreateReplacement(RecoverLoc, + IDecl->getNameAsString()); + Id = IDecl->getIdentifier(); + } + } + return dyn_cast_or_null<ObjCInterfaceDecl>(IDecl); } diff --git a/lib/Sema/SemaDeclObjC.cpp b/lib/Sema/SemaDeclObjC.cpp index beadb588f3..6ff898970a 100644 --- a/lib/Sema/SemaDeclObjC.cpp +++ b/lib/Sema/SemaDeclObjC.cpp @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// #include "Sema.h" +#include "Lookup.h" #include "clang/Sema/ExternalSemaSource.h" #include "clang/AST/Expr.h" #include "clang/AST/ASTContext.h" @@ -133,6 +134,17 @@ ActOnStartClassInterface(SourceLocation AtInterfaceLoc, if (SuperName) { // Check if a different kind of symbol declared in this scope. PrevDecl = LookupSingleName(TUScope, SuperName, LookupOrdinaryName); + + if (!PrevDecl) { + // Try to correct for a typo in the superclass name. + LookupResult R(*this, SuperName, SuperLoc, LookupOrdinaryName); + if (CorrectTypo(R, TUScope, 0) && + (PrevDecl = R.getAsSingle<ObjCInterfaceDecl>())) { + Diag(SuperLoc, diag::err_undef_superclass_suggest) + << SuperName << ClassName << PrevDecl->getDeclName(); + } + } + if (PrevDecl == IDecl) { Diag(SuperLoc, diag::err_recursive_superclass) << SuperName << ClassName << SourceRange(AtInterfaceLoc, ClassLoc); @@ -317,6 +329,16 @@ Sema::FindProtocolDeclaration(bool WarnOnDeclarations, for (unsigned i = 0; i != NumProtocols; ++i) { ObjCProtocolDecl *PDecl = LookupProtocol(ProtocolId[i].first); if (!PDecl) { + LookupResult R(*this, ProtocolId[i].first, ProtocolId[i].second, + LookupObjCProtocolName); + if (CorrectTypo(R, TUScope, 0) && + (PDecl = R.getAsSingle<ObjCProtocolDecl>())) { + Diag(ProtocolId[i].second, diag::err_undeclared_protocol_suggest) + << ProtocolId[i].first << R.getLookupName(); + } + } + + if (!PDecl) { Diag(ProtocolId[i].second, diag::err_undeclared_protocol) << ProtocolId[i].first; continue; @@ -568,7 +590,7 @@ ActOnStartCategoryInterface(SourceLocation AtInterfaceLoc, // FIXME: PushOnScopeChains? CurContext->addDecl(CDecl); - ObjCInterfaceDecl *IDecl = getObjCInterfaceDecl(ClassName); + ObjCInterfaceDecl *IDecl = getObjCInterfaceDecl(ClassName, ClassLoc); /// Check that class of this category is already completely declared. if (!IDecl || IDecl->isForwardDecl()) { CDecl->setInvalidDecl(); @@ -616,7 +638,7 @@ Sema::DeclPtrTy Sema::ActOnStartCategoryImplementation( SourceLocation AtCatImplLoc, IdentifierInfo *ClassName, SourceLocation ClassLoc, IdentifierInfo *CatName, SourceLocation CatLoc) { - ObjCInterfaceDecl *IDecl = getObjCInterfaceDecl(ClassName); + ObjCInterfaceDecl *IDecl = getObjCInterfaceDecl(ClassName, ClassLoc); ObjCCategoryDecl *CatIDecl = 0; if (IDecl) { CatIDecl = IDecl->FindCategoryDeclaration(CatName); diff --git a/lib/Sema/SemaExpr.cpp b/lib/Sema/SemaExpr.cpp index feb6b2b666..a8be333967 100644 --- a/lib/Sema/SemaExpr.cpp +++ b/lib/Sema/SemaExpr.cpp @@ -1059,6 +1059,16 @@ Sema::OwningExprResult Sema::ActOnIdExpression(Scope *S, assert(!R.empty() && "DiagnoseEmptyLookup returned false but added no results"); + + // If we found an Objective-C instance variable, let + // LookupInObjCMethod build the appropriate expression to + // reference the ivar. + if (ObjCIvarDecl *Ivar = R.getAsSingle<ObjCIvarDecl>()) { + R.clear(); + OwningExprResult E(LookupInObjCMethod(R, S, Ivar->getIdentifier())); + assert(E.isInvalid() || E.get()); + return move(E); + } } } @@ -2848,6 +2858,20 @@ Sema::LookupMemberExpr(LookupResult &R, Expr *&BaseExpr, ObjCInterfaceDecl *ClassDeclared; ObjCIvarDecl *IV = IDecl->lookupInstanceVariable(Member, ClassDeclared); + if (!IV) { + // Attempt to correct for typos in ivar names. + LookupResult Res(*this, R.getLookupName(), R.getNameLoc(), + LookupMemberName); + if (CorrectTypo(Res, 0, 0, IDecl) && + (IV = Res.getAsSingle<ObjCIvarDecl>())) { + Diag(R.getNameLoc(), + diag::err_typecheck_member_reference_ivar_suggest) + << IDecl->getDeclName() << MemberName << IV->getDeclName() + << CodeModificationHint::CreateReplacement(R.getNameLoc(), + IV->getNameAsString()); + } + } + if (IV) { // If the decl being referenced had an error, return an error for this // sub-expr without emitting another error, in order to avoid cascading @@ -3014,6 +3038,20 @@ Sema::LookupMemberExpr(LookupResult &R, Expr *&BaseExpr, return Owned(new (Context) ObjCImplicitSetterGetterRefExpr(Getter, PType, Setter, MemberLoc, BaseExpr)); } + + // Attempt to correct for typos in property names. + LookupResult Res(*this, R.getLookupName(), R.getNameLoc(), + LookupOrdinaryName); + if (CorrectTypo(Res, 0, 0, IFace, false, OPT) && + Res.getAsSingle<ObjCPropertyDecl>()) { + Diag(R.getNameLoc(), diag::err_property_not_found_suggest) + << MemberName << BaseType << Res.getLookupName() + << CodeModificationHint::CreateReplacement(R.getNameLoc(), + Res.getLookupName().getAsString()); + return LookupMemberExpr(Res, BaseExpr, IsArrow, OpLoc, SS, + FirstQualifierInScope, ObjCImpDecl); + } + return ExprError(Diag(MemberLoc, diag::err_property_not_found) << MemberName << BaseType); } diff --git a/lib/Sema/SemaExprObjC.cpp b/lib/Sema/SemaExprObjC.cpp index 2e31e47645..85889fa5d6 100644 --- a/lib/Sema/SemaExprObjC.cpp +++ b/lib/Sema/SemaExprObjC.cpp @@ -287,7 +287,8 @@ Action::OwningExprResult Sema::ActOnClassPropertyRefExpr( SourceLocation &receiverNameLoc, SourceLocation &propertyNameLoc) { - ObjCInterfaceDecl *IFace = getObjCInterfaceDecl(&receiverName); + IdentifierInfo *receiverNamePtr = &receiverName; + ObjCInterfaceDecl *IFace = getObjCInterfaceDecl(receiverNamePtr); // Search for a declared property first. @@ -400,7 +401,7 @@ Sema::ExprResult Sema::ActOnClassMessage( return Diag(receiverLoc, diag::err_undeclared_var_use) << receiverName; } } else - ClassDecl = getObjCInterfaceDecl(receiverName); + ClassDecl = getObjCInterfaceDecl(receiverName, receiverLoc); // The following code allows for the following GCC-ism: // diff --git a/lib/Sema/SemaLookup.cpp b/lib/Sema/SemaLookup.cpp index b86b31896c..1f2943cb1f 100644 --- a/lib/Sema/SemaLookup.cpp +++ b/lib/Sema/SemaLookup.cpp @@ -1913,7 +1913,7 @@ static void LookupVisibleDecls(DeclContext *Ctx, LookupResult &Result, } } - // Traverse the contexts of inherited classes. + // Traverse the contexts of inherited C++ classes. if (CXXRecordDecl *Record = dyn_cast<CXXRecordDecl>(Ctx)) { for (CXXRecordDecl::base_class_iterator B = Record->bases_begin(), BEnd = Record->bases_end(); @@ -1955,7 +1955,42 @@ static void LookupVisibleDecls(DeclContext *Ctx, LookupResult &Result, } } - // FIXME: Look into base classes in Objective-C! + // Traverse the contexts of Objective-C classes. + if (ObjCInterfaceDecl *IFace = dyn_cast<ObjCInterfaceDecl>(Ctx)) { + // Traverse categories. + for (ObjCCategoryDecl *Category = IFace->getCategoryList(); + Category; Category = Category->getNextClassCategory()) { + ShadowContextRAII Shadow(Visited); + LookupVisibleDecls(Category, Result, QualifiedNameLookup, Consumer, + Visited); + } + + // Traverse protocols. + for (ObjCInterfaceDecl::protocol_iterator I = IFace->protocol_begin(), + E = IFace->protocol_end(); I != E; ++I) { + ShadowContextRAII Shadow(Visited); + LookupVisibleDecls(*I, Result, QualifiedNameLookup, Consumer, Visited); + } + + // Traverse the superclass. + if (IFace->getSuperClass()) { + ShadowContextRAII Shadow(Visited); + LookupVisibleDecls(IFace->getSuperClass(), Result, QualifiedNameLookup, + Consumer, Visited); + } + } else if (ObjCProtocolDecl *Protocol = dyn_cast<ObjCProtocolDecl>(Ctx)) { + for (ObjCProtocolDecl::protocol_iterator I = Protocol->protocol_begin(), + E = Protocol->protocol_end(); I != E; ++I) { + ShadowContextRAII Shadow(Visited); + LookupVisibleDecls(*I, Result, QualifiedNameLookup, Consumer, Visited); + } + } else if (ObjCCategoryDecl *Category = dyn_cast<ObjCCategoryDecl>(Ctx)) { + for (ObjCCategoryDecl::protocol_iterator I = Category->protocol_begin(), + E = Category->protocol_end(); I != E; ++I) { + ShadowContextRAII Shadow(Visited); + LookupVisibleDecls(*I, Result, QualifiedNameLookup, Consumer, Visited); + } + } } static void LookupVisibleDecls(Scope *S, LookupResult &Result, @@ -1975,6 +2010,22 @@ static void LookupVisibleDecls(Scope *S, LookupResult &Result, for (DeclContext *Ctx = Entity; Ctx && Ctx->getPrimaryContext() != OuterCtx; Ctx = Ctx->getLookupParent()) { + if (ObjCMethodDecl *Method = dyn_cast<ObjCMethodDecl>(Ctx)) { + if (Method->isInstanceMethod()) { + // For instance methods, look for ivars in the method's interface. + LookupResult IvarResult(Result.getSema(), Result.getLookupName(), + Result.getNameLoc(), Sema::LookupMemberName); + ObjCInterfaceDecl *IFace = Method->getClassInterface(); + LookupVisibleDecls(IFace, IvarResult, /*QualifiedNameLookup=*/false, + Consumer, Visited); + } + + // We've already performed all of the name lookup that we need + // to for Objective-C methods; the next context will be the + // outer scope. + break; + } + if (Ctx->isFunctionOrMethod()) continue; @@ -2139,11 +2190,15 @@ void TypoCorrectionConsumer::FoundDecl(NamedDecl *ND, NamedDecl *Hiding) { /// \param EnteringContext whether we're entering the context described by /// the nested-name-specifier SS. /// +/// \param OPT when non-NULL, the search for visible declarations will +/// also walk the protocols in the qualified interfaces of \p OPT. +/// /// \returns true if the typo was corrected, in which case the \p Res /// structure will contain the results of name lookup for the /// corrected name. Otherwise, returns false. bool Sema::CorrectTypo(LookupResult &Res, Scope *S, const CXXScopeSpec *SS, - DeclContext *MemberContext, bool EnteringContext) { + DeclContext *MemberContext, bool EnteringContext, + const ObjCObjectPointerType *OPT) { // We only attempt to correct typos for identifiers. IdentifierInfo *Typo = Res.getLookupName().getAsIdentifierInfo(); if (!Typo) @@ -2160,9 +2215,17 @@ bool Sema::CorrectTypo(LookupResult &Res, Scope *S, const CXXScopeSpec *SS, return false; TypoCorrectionConsumer Consumer(Typo); - if (MemberContext) + if (MemberContext) { LookupVisibleDecls(MemberContext, Res.getLookupKind(), Consumer); - else if (SS && SS->isSet()) { + + // Look in qualified interfaces. + if (OPT) { + for (ObjCObjectPointerType::qual_iterator + I = OPT->qual_begin(), E = OPT->qual_end(); + I != E; ++I) + LookupVisibleDecls(*I, Res.getLookupKind(), Consumer); + } + } else if (SS && SS->isSet()) { DeclContext *DC = computeDeclContext(*SS, EnteringContext); if (!DC) return false; @@ -2179,10 +2242,22 @@ bool Sema::CorrectTypo(LookupResult &Res, Scope *S, const CXXScopeSpec *SS, // have overloads of that name, though). TypoCorrectionConsumer::iterator I = Consumer.begin(); DeclarationName BestName = (*I)->getDeclName(); + + // If we've found an Objective-C ivar or property, don't perform + // name lookup again; we'll just return the result directly. + NamedDecl *FoundBest = 0; + if (isa<ObjCIvarDecl>(*I) || isa<ObjCPropertyDecl>(*I)) + FoundBest = *I; ++I; for(TypoCorrectionConsumer::iterator IEnd = Consumer.end(); I != IEnd; ++I) { if (BestName != (*I)->getDeclName()) return false; + + // FIXME: If there are both ivars and properties of the same name, + // don't return both because the callee can't handle two + // results. We really need to separate ivar lookup from property + // lookup to avoid this problem. + FoundBest = 0; } // BestName is the closest viable name to what the user @@ -2197,8 +2272,16 @@ bool Sema::CorrectTypo(LookupResult &Res, Scope *S, const CXXScopeSpec *SS, // success if we found something that was not ambiguous. Res.clear(); Res.setLookupName(BestName); - if (MemberContext) + + // If we found an ivar or property, add that result; no further + // lookup is required. + if (FoundBest) + Res.addDecl(FoundBest); + // If we're looking into the context of a member, perform qualified + // name lookup on the best name. + else if (MemberContext) LookupQualifiedName(Res, MemberContext); + // Perform lookup as if we had just parsed the best name. else LookupParsedName(Res, S, SS, /*AllowBuiltinCreation=*/false, EnteringContext); diff --git a/test/FixIt/typo.m b/test/FixIt/typo.m index f88d315da7..cff5dd84ea 100644 --- a/test/FixIt/typo.m +++ b/test/FixIt/typo.m @@ -1,9 +1,89 @@ // RUN: %clang_cc1 -fsyntax-only -verify %s +// FIXME: the test below isn't testing quite what we want... // RUN: %clang_cc1 -fsyntax-only -fixit -o - %s | %clang_cc1 -fsyntax-only -pedantic -Werror -x objective-c - @interface NSString ++ (int)method:(int)x; @end void test() { + // FIXME: not providing fix-its NSstring *str = @"A string"; // expected-error{{use of undeclared identifier 'NSstring'; did you mean 'NSString'?}} } + +@protocol P1 +@property int *sprop; +@end + +@interface A +{ + int his_ivar; + float wibble; +} + +@property int his_prop; +@end + +@interface B : A <P1> +{ + int her_ivar; +} + +@property int her_prop; +- (void)inst_method1:(int)a; ++ (void)class_method1; +@end + +@implementation A +@synthesize his_prop = his_ivar; +@end + +@implementation B +@synthesize her_prop = her_ivar; + +-(void)inst_method1:(int)a { + herivar = a; // expected-error{{use of undeclared identifier 'herivar'; did you mean 'her_ivar'?}} + hisivar = a; // expected-error{{use of undeclared identifier 'hisivar'; did you mean 'his_ivar'?}} + self->herivar = a; // expected-error{{'B' does not have a member named 'herivar'; did you mean 'her_ivar'?}} + self->hisivar = a; // expected-error{{'B' does not have a member named 'hisivar'; did you mean 'his_ivar'?}} + self.hisprop = 0; // expected-error{{property 'hisprop' not found on object of type 'B *'; did you mean 'his_prop'?}} + self.herprop = 0; // expected-error{{property 'herprop' not found on object of type 'B *'; did you mean 'her_prop'?}} + self.s_prop = 0; // expected-error{{property 's_prop' not found on object of type 'B *'; did you mean 'sprop'?}} +} + ++(void)class_method1 { +} +@end + +void test_message_send(B* b) { + // FIXME: Not providing fix-its + [NSstring method:17]; // expected-error{{use of undeclared identifier 'NSstring'; did you mean 'NSString'?}} +} + +@interface Collide +{ +@public + int value; +} + +@property int value; +@end + +@implementation Collide +@synthesize value = value; +@end + +void test2(Collide *a) { + a.valu = 17; // expected-error{{property 'valu' not found on object of type 'Collide *'; did you mean 'value'?}} + a->vale = 17; // expected-error{{'Collide' does not have a member named 'vale'; did you mean 'value'?}} +} + +@interface Derived : Collid // expected-error{{cannot find interface declaration for 'Collid', superclass of 'Derived'; did you mean 'Collide'?}} +@end + +@protocol NetworkSocket +- (int)send:(void*)buffer bytes:(int)bytes; +@end + +@interface IPv8 <Network_Socket> // expected-error{{cannot find protocol declaration for 'Network_Socket'; did you mean 'NetworkSocket'?}} +@end diff --git a/test/SemaObjC/category-1.m b/test/SemaObjC/category-1.m index 17c6b46202..33e4646837 100644 --- a/test/SemaObjC/category-1.m +++ b/test/SemaObjC/category-1.m @@ -29,7 +29,7 @@ @interface MyClass1 (Category) <p2, p3> @end // expected-warning {{cannot find protocol definition for 'p2'}} -@interface MyClass (Category) @end // expected-error {{cannot find interface declaration for 'MyClass'}} +@interface UnknownClass (Category) @end // expected-error {{cannot find interface declaration for 'UnknownClass'}} @class MyClass2; diff --git a/test/SemaObjC/undef-class-messagin-error.m b/test/SemaObjC/undef-class-messagin-error.m index 0a400dd39f..2a6d240840 100644 --- a/test/SemaObjC/undef-class-messagin-error.m +++ b/test/SemaObjC/undef-class-messagin-error.m @@ -4,10 +4,10 @@ + (int) flashCache; @end -@interface Child (Categ) // expected-error {{cannot find interface declaration for 'Child'}} +@interface Child (Categ) // expected-error {{cannot find interface declaration for 'Child'; did you mean '_Child'?}} + (int) flushCache2; @end -@implementation Child (Categ) // expected-error {{cannot find interface declaration for 'Child'}} +@implementation OtherChild (Categ) // expected-error {{cannot find interface declaration for 'OtherChild'}} + (int) flushCache2 { [super flashCache]; } // expected-error {{no @interface declaration found in class messaging of 'flushCache2'}} @end |