]> rtime.felk.cvut.cz Git - boost-statechart-viewer.git/blob - src/visualizer.cpp
Warning missing typedef for react method
[boost-statechart-viewer.git] / src / visualizer.cpp
1 /** @file */
2 ////////////////////////////////////////////////////////////////////////////////////////
3 //
4 //    This file is part of Boost Statechart Viewer.
5 //
6 //    Boost Statechart Viewer is free software: you can redistribute it and/or modify
7 //    it under the terms of the GNU General Public License as published by
8 //    the Free Software Foundation, either version 3 of the License, or
9 //    (at your option) any later version.
10 //
11 //    Boost Statechart Viewer is distributed in the hope that it will be useful,
12 //    but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 //    GNU General Public License for more details.
15 //
16 //    You should have received a copy of the GNU General Public License
17 //    along with Boost Statechart Viewer.  If not, see <http://www.gnu.org/licenses/>.
18 //
19 ////////////////////////////////////////////////////////////////////////////////////////
20
21 //standard header files
22 #include <iomanip>
23 #include <fstream>
24 #include <map>
25 #include <vector>
26
27 //LLVM Header files
28 #include "llvm/Support/raw_ostream.h"
29 #include "llvm/Support/raw_os_ostream.h"
30
31 //clang header files
32 #include "clang/AST/ASTConsumer.h"
33 #include "clang/AST/ASTContext.h"
34 #include "clang/AST/CXXInheritance.h"
35 #include "clang/AST/RecursiveASTVisitor.h"
36 #include "clang/Frontend/CompilerInstance.h"
37 #include "clang/Frontend/FrontendPluginRegistry.h"
38
39 using namespace clang;
40 using namespace std;
41
42 namespace Model
43 {
44
45     inline int getIndentLevelIdx() {
46         static int i = ios_base::xalloc();
47         return i;
48     }
49
50     ostream& indent(ostream& os) { os << setw(2*os.iword(getIndentLevelIdx())) << ""; return os; }
51     ostream& indent_inc(ostream& os) { os.iword(getIndentLevelIdx())++; return os; }
52     ostream& indent_dec(ostream& os) { os.iword(getIndentLevelIdx())--; return os; }
53
54     class State;
55
56     class Context : public map<string, State*> {
57     public:
58         iterator add(State *state);
59         Context *findContext(const string &name);
60     };
61
62     class State : public Context
63     {
64         string initialInnerState;
65         list<string> defferedEvents;
66         bool noTypedef;
67     public:
68         const string name;
69         explicit State(string name) : noTypedef(false), name(name) {}
70         void setInitialInnerState(string name) { initialInnerState = name; }
71         void addDeferredEvent(const string &name) { defferedEvents.push_back(name); }
72         void setNoTypedef() { noTypedef = true;}
73         friend ostream& operator<<(ostream& os, const State& s);
74     };
75
76
77     Context::iterator Context::add(State *state)
78     {
79         pair<iterator, bool> ret =  insert(value_type(state->name, state));
80         return ret.first;
81     }
82
83     Context *Context::findContext(const string &name)
84     {
85         iterator i = find(name), e;
86         if (i != end())
87             return i->second;
88         for (i = begin(), e = end(); i != e; ++i) {
89             Context *c = i->second->findContext(name);
90             if (c)
91                 return c;
92         }
93         return 0;
94     }
95
96     ostream& operator<<(ostream& os, const Context& c);
97
98     ostream& operator<<(ostream& os, const State& s)
99     {
100         string label = s.name;
101         for (list<string>::const_iterator i = s.defferedEvents.begin(), e = s.defferedEvents.end(); i != e; ++i)
102             label.append("<br />").append(*i).append(" / defer");
103         if (s.noTypedef) os << indent << s.name << " [label=<" << label << ">, color=\"red\"]\n";
104         else os << indent << s.name << " [label=<" << label << ">]\n";
105         if (s.size()) {
106             os << indent << s.name << " -> " << s.initialInnerState << " [style = dashed]\n";
107             os << indent << "subgraph cluster_" << s.name << " {\n" << indent_inc;
108             os << indent << "label = \"" << s.name << "\"\n";
109             os << indent << s.initialInnerState << " [peripheries=2]\n";
110             os << static_cast<Context>(s);
111             os << indent_dec << indent << "}\n";
112         }
113         return os;
114     }
115
116
117     ostream& operator<<(ostream& os, const Context& c)
118     {
119         for (Context::const_iterator i = c.begin(), e = c.end(); i != e; i++) {
120             os << *i->second;
121         }
122         return os;
123     }
124
125
126     class Transition
127     {
128     public:
129         const string src, dst, event;
130         Transition(string src, string dst, string event) : src(src), dst(dst), event(event) {}
131     };
132
133     ostream& operator<<(ostream& os, const Transition& t)
134     {
135         os << indent << t.src << " -> " << t.dst << " [label = \"" << t.event << "\"]\n";
136         return os;
137     }
138
139
140     class Machine : public Context
141     {
142     protected:
143         string initial_state;
144     public:
145         const string name;
146         explicit Machine(string name) : name(name) {}
147
148         void setInitialState(string name) { initial_state = name; }
149
150         friend ostream& operator<<(ostream& os, const Machine& m);
151     };
152
153     ostream& operator<<(ostream& os, const Machine& m)
154     {
155         os << indent << "subgraph " << m.name << " {\n" << indent_inc;
156         os << indent << m.initial_state << " [peripheries=2]\n";
157         os << static_cast<Context>(m);
158         os << indent_dec << indent << "}\n";
159         return os;
160     }
161
162
163     class Model : public map<string, Machine>
164     {
165         Context undefined;      // For forward-declared state classes
166     public:
167         list< Transition*> transitions;
168
169         iterator add(const Machine &m)
170         {
171             pair<iterator, bool> ret =  insert(value_type(m.name, m));
172             return ret.first;
173         }
174
175         void addUndefinedState(State *m)
176         {
177             undefined[m->name] = m;
178         }
179
180
181         Context *findContext(const string &name)
182         {
183             Context::iterator ci = undefined.find(name);
184             if (ci != undefined.end())
185                 return ci->second;
186             iterator i = find(name), e;
187             if (i != end())
188                 return &i->second;
189             for (i = begin(), e = end(); i != e; ++i) {
190                 Context *c = i->second.findContext(name);
191                 if (c)
192                     return c;
193             }
194             return 0;
195         }
196
197         State *findState(const string &name)
198         {
199             for (iterator i = begin(), e = end(); i != e; ++i) {
200                 Context *c = i->second.findContext(name);
201                 if (c)
202                     return static_cast<State*>(c);
203             }
204             return 0;
205         }
206
207
208         State *removeFromUndefinedContexts(const string &name)
209         {
210             Context::iterator ci = undefined.find(name);
211             if (ci == undefined.end())
212                 return 0;
213             undefined.erase(ci);
214             return ci->second;
215         }
216
217         void write_as_dot_file(string fn)
218         {
219             ofstream f(fn.c_str());
220             f << "digraph statecharts {\n" << indent_inc;
221             for (iterator i = begin(), e = end(); i != e; i++)
222                 f << i->second;
223             for (list<Transition*>::iterator t = transitions.begin(), e = transitions.end(); t != e; ++t)
224                 f << **t;
225             f << indent_dec << "}\n";
226         }
227     };
228 };
229
230
231 class MyCXXRecordDecl : public CXXRecordDecl
232 {
233     static bool FindBaseClassString(const CXXBaseSpecifier *Specifier,
234                                     CXXBasePath &Path,
235                                     void *qualName)
236     {
237         string qn(static_cast<const char*>(qualName));
238         const RecordType *rt = Specifier->getType()->getAs<RecordType>();
239         assert(rt);
240         TagDecl *canon = rt->getDecl()->getCanonicalDecl();
241         return canon->getQualifiedNameAsString() == qn;
242     }
243
244 public:
245     bool isDerivedFrom(const char *baseStr, CXXBaseSpecifier const **Base = 0) const {
246         CXXBasePaths Paths(/*FindAmbiguities=*/false, /*RecordPaths=*/!!Base, /*DetectVirtual=*/false);
247         Paths.setOrigin(const_cast<MyCXXRecordDecl*>(this));
248         if (!lookupInBases(&FindBaseClassString, const_cast<char*>(baseStr), Paths))
249             return false;
250         if (Base)
251             *Base = Paths.front().back().Base;
252         return true;
253     }
254 };
255
256 class FindTransitVisitor : public RecursiveASTVisitor<FindTransitVisitor>
257 {
258     Model::Model &model;
259     const CXXRecordDecl *SrcState;
260     const Type *EventType;
261 public:
262     explicit FindTransitVisitor(Model::Model &model, const CXXRecordDecl *SrcState, const Type *EventType)
263         : model(model), SrcState(SrcState), EventType(EventType) {}
264
265     bool VisitMemberExpr(MemberExpr *E) {
266         if (E->getMemberNameInfo().getAsString() != "transit")
267             return true;
268         if (E->hasExplicitTemplateArgs()) {
269             const Type *DstStateType = E->getExplicitTemplateArgs()[0].getArgument().getAsType().getTypePtr();
270             CXXRecordDecl *DstState = DstStateType->getAsCXXRecordDecl();
271             CXXRecordDecl *Event = EventType->getAsCXXRecordDecl();
272             Model::Transition *T = new Model::Transition(SrcState->getName(), DstState->getName(), Event->getName());
273             model.transitions.push_back(T);
274         }
275         return true;
276     }
277 };
278
279 class Visitor : public RecursiveASTVisitor<Visitor>
280 {
281     ASTContext *ASTCtx;
282     Model::Model &model;
283     DiagnosticsEngine &Diags;
284     unsigned diag_unhandled_reaction_type, diag_unhandled_reaction_decl,
285         diag_found_state, diag_found_statemachine, diag_no_history, diag_missing_reaction, diag_warning;
286     std::vector<bool> reactMethodVector;
287
288 public:
289     bool shouldVisitTemplateInstantiations() const { return true; }
290
291     explicit Visitor(ASTContext *Context, Model::Model &model, DiagnosticsEngine &Diags)
292         : ASTCtx(Context), model(model), Diags(Diags)
293     {
294         diag_found_statemachine =
295             Diags.getCustomDiagID(DiagnosticsEngine::Note, "Found statemachine '%0'");
296         diag_found_state =
297             Diags.getCustomDiagID(DiagnosticsEngine::Note, "Found state '%0'");
298         diag_unhandled_reaction_type =
299             Diags.getCustomDiagID(DiagnosticsEngine::Error, "Unhandled reaction type '%0'");
300         diag_unhandled_reaction_decl =
301             Diags.getCustomDiagID(DiagnosticsEngine::Error, "Unhandled reaction decl '%0'");
302         diag_unhandled_reaction_decl =
303             Diags.getCustomDiagID(DiagnosticsEngine::Error, "History is not yet supported");
304         diag_missing_reaction =
305             Diags.getCustomDiagID(DiagnosticsEngine::Error, "Missing react method for event '%0'");
306         diag_warning =
307             Diags.getCustomDiagID(DiagnosticsEngine::Warning, "'%0' %1");
308     }
309
310     DiagnosticBuilder Diag(SourceLocation Loc, unsigned DiagID) { return Diags.Report(Loc, DiagID); }
311
312     void checkAllReactMethods(const CXXRecordDecl *SrcState) 
313     {
314         unsigned i = 0;
315         IdentifierInfo& II = ASTCtx->Idents.get("react");
316         for (DeclContext::lookup_const_result ReactRes = SrcState->lookup(DeclarationName(&II));
317              ReactRes.first != ReactRes.second; ++ReactRes.first) {
318             if (i<reactMethodVector.size()) {
319                 if (reactMethodVector[i] == true) {
320                     CXXMethodDecl *React = dyn_cast<CXXMethodDecl>(*ReactRes.first);
321                     Diag(React->getParamDecl(0)->getLocStart(), diag_warning) 
322                         << React->getParamDecl(0)->getType().getAsString() << " missing in typedef";
323                 }
324             } else {
325                 CXXMethodDecl *React = dyn_cast<CXXMethodDecl>(*ReactRes.first);
326                 Diag(React->getParamDecl(0)->getLocStart(), diag_warning) 
327                     << React->getParamDecl(0)->getType().getAsString() << " missing in typedef";
328             }
329             i++;
330         }
331         reactMethodVector.clear();
332     }
333     
334     bool HandleCustomReaction(const CXXRecordDecl *SrcState, const Type *EventType)
335     {
336         unsigned i = 0;
337         IdentifierInfo& II = ASTCtx->Idents.get("react");
338         // TODO: Lookup for react even in base classes - probably by using Sema::LookupQualifiedName()
339         for (DeclContext::lookup_const_result ReactRes = SrcState->lookup(DeclarationName(&II));
340              ReactRes.first != ReactRes.second; ++ReactRes.first) {
341             if (CXXMethodDecl *React = dyn_cast<CXXMethodDecl>(*ReactRes.first)) {
342                 if (React->getNumParams() >= 1) {
343                     const ParmVarDecl *p = React->getParamDecl(0);
344                     const Type *ParmType = p->getType().getTypePtr();
345                     if (i == reactMethodVector.size()) reactMethodVector.push_back(false);
346                     if (ParmType->isLValueReferenceType())
347                         ParmType = dyn_cast<LValueReferenceType>(ParmType)->getPointeeType().getTypePtr();
348                     if (ParmType == EventType) {
349                         FindTransitVisitor(model, SrcState, EventType).TraverseStmt(React->getBody());
350                         reactMethodVector[i] = true;
351                         return true;
352                     }
353                 } else
354                     Diag(React->getLocStart(), diag_warning)
355                         << React << "has not a parameter";
356             } else
357                 Diag((*ReactRes.first)->getSourceRange().getBegin(), diag_warning)
358                     << (*ReactRes.first)->getDeclKindName() << "is not supported as react method";
359             i++;
360         }
361         return false;
362     }
363
364     void HandleReaction(const Type *T, const SourceLocation Loc, CXXRecordDecl *SrcState)
365     {
366         // TODO: Improve Loc tracking
367         if (const ElaboratedType *ET = dyn_cast<ElaboratedType>(T))
368             HandleReaction(ET->getNamedType().getTypePtr(), Loc, SrcState);
369         else if (const TemplateSpecializationType *TST = dyn_cast<TemplateSpecializationType>(T)) {
370             string name = TST->getTemplateName().getAsTemplateDecl()->getQualifiedNameAsString();
371             if (name == "boost::statechart::transition") {
372                 const Type *EventType = TST->getArg(0).getAsType().getTypePtr();
373                 const Type *DstStateType = TST->getArg(1).getAsType().getTypePtr();
374                 CXXRecordDecl *Event = EventType->getAsCXXRecordDecl();
375                 CXXRecordDecl *DstState = DstStateType->getAsCXXRecordDecl();
376
377                 Model::Transition *T = new Model::Transition(SrcState->getName(), DstState->getName(), Event->getName());
378                 model.transitions.push_back(T);
379             } else if (name == "boost::statechart::custom_reaction") {
380                 const Type *EventType = TST->getArg(0).getAsType().getTypePtr();
381                 if (!HandleCustomReaction(SrcState, EventType)) {
382                     Diag(SrcState->getLocation(), diag_missing_reaction) << EventType->getAsCXXRecordDecl()->getName();
383                 }
384             } else if (name == "boost::statechart::deferral") {
385                 const Type *EventType = TST->getArg(0).getAsType().getTypePtr();
386                 CXXRecordDecl *Event = EventType->getAsCXXRecordDecl();
387
388                 Model::State *s = model.findState(SrcState->getName());
389                 assert(s);
390                 s->addDeferredEvent(Event->getName());
391             } else if (name == "boost::mpl::list") {
392                 for (TemplateSpecializationType::iterator Arg = TST->begin(), End = TST->end(); Arg != End; ++Arg)
393                     HandleReaction(Arg->getAsType().getTypePtr(), Loc, SrcState);
394             } else
395                 Diag(Loc, diag_unhandled_reaction_type) << name;
396         } else
397             Diag(Loc, diag_unhandled_reaction_type) << T->getTypeClassName();
398     }
399
400     void HandleReaction(const NamedDecl *Decl, CXXRecordDecl *SrcState)
401     {
402         if (const TypedefDecl *r = dyn_cast<TypedefDecl>(Decl))
403             HandleReaction(r->getCanonicalDecl()->getUnderlyingType().getTypePtr(),
404                            r->getLocStart(), SrcState);
405         else
406             Diag(Decl->getLocation(), diag_unhandled_reaction_decl) << Decl->getDeclKindName();
407         checkAllReactMethods(SrcState);
408     }
409
410     TemplateArgumentLoc getTemplateArgLoc(const TypeLoc &T, unsigned ArgNum, bool ignore)
411     {
412         if (const ElaboratedTypeLoc *ET = dyn_cast<ElaboratedTypeLoc>(&T))
413             return getTemplateArgLoc(ET->getNamedTypeLoc(), ArgNum, ignore);
414         else if (const TemplateSpecializationTypeLoc *TST = dyn_cast<TemplateSpecializationTypeLoc>(&T)) {
415             if (TST->getNumArgs() >= ArgNum+1) {
416                 return TST->getArgLoc(ArgNum);
417             } else
418                 if (!ignore)
419                     Diag(TST->getBeginLoc(), diag_warning) << TST->getType()->getTypeClassName() << "has not enough arguments" << TST->getSourceRange();
420         } else
421             Diag(T.getBeginLoc(), diag_warning) << T.getType()->getTypeClassName() << "type as template argument is not supported" << T.getSourceRange();
422         return TemplateArgumentLoc();
423     }
424
425     TemplateArgumentLoc getTemplateArgLocOfBase(const CXXBaseSpecifier *Base, unsigned ArgNum, bool ignore) {
426         return getTemplateArgLoc(Base->getTypeSourceInfo()->getTypeLoc(), ArgNum, ignore);
427     }
428
429     CXXRecordDecl *getTemplateArgDeclOfBase(const CXXBaseSpecifier *Base, unsigned ArgNum, TemplateArgumentLoc &Loc, bool ignore = false) {
430         Loc = getTemplateArgLocOfBase(Base, ArgNum, ignore);
431         switch (Loc.getArgument().getKind()) {
432         case TemplateArgument::Type:
433             return Loc.getTypeSourceInfo()->getType()->getAsCXXRecordDecl();
434         case TemplateArgument::Null:
435             // Diag() was already called
436             break;
437         default:
438             Diag(Loc.getSourceRange().getBegin(), diag_warning) << Loc.getArgument().getKind() << "unsupported kind" << Loc.getSourceRange();
439         }
440         return 0;
441     }
442
443     CXXRecordDecl *getTemplateArgDeclOfBase(const CXXBaseSpecifier *Base, unsigned ArgNum, bool ignore = false) {
444         TemplateArgumentLoc Loc;
445         return getTemplateArgDeclOfBase(Base, ArgNum, Loc, ignore);
446     }
447
448     void handleSimpleState(CXXRecordDecl *RecordDecl, const CXXBaseSpecifier *Base)
449     {
450         int typedef_num = 0;
451         string name(RecordDecl->getName()); //getQualifiedNameAsString());
452         Diag(RecordDecl->getLocStart(), diag_found_state) << name;
453
454         Model::State *state;
455         // Either we saw a reference to forward declared state
456         // before, or we create a new state.
457         if (!(state = model.removeFromUndefinedContexts(name)))
458             state = new Model::State(name);
459
460         CXXRecordDecl *Context = getTemplateArgDeclOfBase(Base, 1);
461         if (Context) {
462             Model::Context *c = model.findContext(Context->getName());
463             if (!c) {
464                 Model::State *s = new Model::State(Context->getName());
465                 model.addUndefinedState(s);
466                 c = s;
467             }
468             c->add(state);
469         }
470         //TODO support more innitial states
471         TemplateArgumentLoc Loc;
472         if (MyCXXRecordDecl *InnerInitialState =
473             static_cast<MyCXXRecordDecl*>(getTemplateArgDeclOfBase(Base, 2, Loc, true))) {
474               if (InnerInitialState->isDerivedFrom("boost::statechart::simple_state") ||
475                 InnerInitialState->isDerivedFrom("boost::statechart::state_machine")) {
476                   state->setInitialInnerState(InnerInitialState->getName());
477             }
478             else
479                 Diag(Loc.getTypeSourceInfo()->getTypeLoc().getLocStart(), diag_warning)
480                     << InnerInitialState->getName() << " as inner initial state is not supported" << Loc.getSourceRange();
481         }
482
483 //          if (CXXRecordDecl *History = getTemplateArgDecl(Base->getType().getTypePtr(), 3))
484 //              Diag(History->getLocStart(), diag_no_history);
485
486         IdentifierInfo& II = ASTCtx->Idents.get("reactions");
487         // TODO: Lookup for reactions even in base classes - probably by using Sema::LookupQualifiedName()
488         for (DeclContext::lookup_result Reactions = RecordDecl->lookup(DeclarationName(&II));
489              Reactions.first != Reactions.second; ++Reactions.first, typedef_num++)
490             HandleReaction(*Reactions.first, RecordDecl);
491         if(typedef_num == 0) {
492             Diag(RecordDecl->getLocStart(), diag_warning)
493                 << RecordDecl->getName() << "state has no typedef for reactions";
494             state->setNoTypedef();
495         }
496     }
497
498     void handleStateMachine(CXXRecordDecl *RecordDecl, const CXXBaseSpecifier *Base)
499     {
500         Model::Machine m(RecordDecl->getName());
501         Diag(RecordDecl->getLocStart(), diag_found_statemachine) << m.name;
502
503         if (MyCXXRecordDecl *InitialState =
504             static_cast<MyCXXRecordDecl*>(getTemplateArgDeclOfBase(Base, 1)))
505             m.setInitialState(InitialState->getName());
506         model.add(m);
507     }
508
509     bool VisitCXXRecordDecl(CXXRecordDecl *Declaration)
510     {
511         if (!Declaration->isCompleteDefinition())
512             return true;
513         if (Declaration->getQualifiedNameAsString() == "boost::statechart::state" ||
514             Declaration->getQualifiedNameAsString() == "TimedState" ||
515             Declaration->getQualifiedNameAsString() == "TimedSimpleState")
516             return true; // This is an "abstract class" not a real state
517
518         MyCXXRecordDecl *RecordDecl = static_cast<MyCXXRecordDecl*>(Declaration);
519         const CXXBaseSpecifier *Base;
520
521         if (RecordDecl->isDerivedFrom("boost::statechart::simple_state", &Base))
522             handleSimpleState(RecordDecl, Base);
523         else if (RecordDecl->isDerivedFrom("boost::statechart::state_machine", &Base))
524             handleStateMachine(RecordDecl, Base);
525         else if (RecordDecl->isDerivedFrom("boost::statechart::event"))
526         {
527             //sc.events.push_back(RecordDecl->getNameAsString());
528         }
529         return true;
530     }
531 };
532
533
534 class VisualizeStatechartConsumer : public clang::ASTConsumer
535 {
536     Model::Model model;
537     Visitor visitor;
538     string destFileName;
539 public:
540     explicit VisualizeStatechartConsumer(ASTContext *Context, std::string destFileName,
541                                          DiagnosticsEngine &D)
542         : visitor(Context, model, D), destFileName(destFileName) {}
543
544     virtual void HandleTranslationUnit(clang::ASTContext &Context) {
545         visitor.TraverseDecl(Context.getTranslationUnitDecl());
546         model.write_as_dot_file(destFileName);
547     }
548 };
549
550 class VisualizeStatechartAction : public PluginASTAction
551 {
552 protected:
553   ASTConsumer *CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) {
554     size_t dot = getCurrentFile().find_last_of('.');
555     std::string dest = getCurrentFile().substr(0, dot);
556     dest.append(".dot");
557     return new VisualizeStatechartConsumer(&CI.getASTContext(), dest, CI.getDiagnostics());
558   }
559
560   bool ParseArgs(const CompilerInstance &CI,
561                  const std::vector<std::string>& args) {
562     for (unsigned i = 0, e = args.size(); i != e; ++i) {
563       llvm::errs() << "Visualizer arg = " << args[i] << "\n";
564
565       // Example error handling.
566       if (args[i] == "-an-error") {
567         DiagnosticsEngine &D = CI.getDiagnostics();
568         unsigned DiagID = D.getCustomDiagID(
569           DiagnosticsEngine::Error, "invalid argument '" + args[i] + "'");
570         D.Report(DiagID);
571         return false;
572       }
573     }
574     if (args.size() && args[0] == "help")
575       PrintHelp(llvm::errs());
576
577     return true;
578   }
579   void PrintHelp(llvm::raw_ostream& ros) {
580     ros << "Help for Visualize Statechart plugin goes here\n";
581   }
582
583 };
584
585 static FrontendPluginRegistry::Add<VisualizeStatechartAction> X("visualize-statechart", "visualize statechart");
586
587 // Local Variables:
588 // c-basic-offset: 4
589 // End: