想法
我一直认为,将代码中的内容以图形化的方式表示出来有助于理解。对于我来说,类之间的继承关系和模块之间的依赖关系是我比较关心的内容。不过,当项目中的代码很多时,手动为感兴趣的内容画图将会消耗很多时间,我觉得需要自动化的工具生成图形。
生成图形方面,可以借助 mermaid 的力量(而且CSDN博客也支持其基本的功能)。这样,剩下的问题就是获得这些信息的途径了。
这篇博客讨论了怎样获取代码中的类继承信息,并转换为mermaid流程图代码。
理论上正统的途径
关于如何获得“代码中的类继承信息”,最理想且严谨的途径应该是静态代码分析。比如使用 Clang ,来生成Token流,以及 抽象语法树 AST。我也尝试下载了Clang并且运行Clang来获得代码文件的信息(详细见【附录】)。
不过,虽然这个方法很严谨而且看起来是最“正统”的方法。但是这让问题变得比较复杂。毕竟,我这里只是想获得类的继承关系。
我的途径
我的想法是不借助Clang的力量,直接对代码文件中的文本进行分析。当然,行为和上者类似,要分析出Token流。不过我这里只关心类声明信息相关的Token流。随后,对Token流进行分析,并收集类继承关系这个信息。
代码
ClassInfoCollector.h
ClassInfoCollector
负责收集信息,基本的接口是Collect(string filePath)
,收集一个代码文件中的信息。
#pragma once
#include<vector>
using namespace std;
//类的信息
struct ClassInfo
{
string ClassName;
vector<string> ParentClasses;
};
//类的继承关系
struct ClassInheritRelationship
{
string childClass;
string parentClass;
};
//类信息收集者
class ClassInfoCollector
{
private://私有成员
vector<ClassInfo> ClassInfoList;
public://公开接口
//收集一个文件中的信息
void Collect(string filePath);
//插入一个信息
void InsertOneInfo(ClassInfo info);
//得到所有的继承关系
vector<ClassInheritRelationship> GetAllClassInheritRelationship();
};
ClassInfoCollector.cpp
ClassInfoCollector
内函数的实现:
#include"ClassInfoCollector.h"
#include"ClassDeclarationInfoSeeker.h"
#include<fstream>
void ClassInfoCollector::Collect(string filePath)
{
ifstream inFile;
inFile.open(filePath);
if (inFile)
{
ClassDeclarationInfoSeeker Seeker(this);
char ch; //待读取字符串
while (inFile.get(ch)) //顺序读取每一个字符
Seeker.ProcessNextOneChar(ch);
}
}
void ClassInfoCollector::InsertOneInfo(ClassInfo info)
{
ClassInfoList.push_back(info);
}
vector<ClassInheritRelationship> ClassInfoCollector::GetAllClassInheritRelationship()
{
vector<ClassInheritRelationship> result;
for (auto info : ClassInfoList)
{
ClassInheritRelationship Relationship;
Relationship.childClass = info.ClassName;
for (auto parent : info.ParentClasses)
{
Relationship.parentClass = parent;
result.push_back(Relationship);
}
}
return result;
}
在Collect
函数中,一个ClassDeclarationInfoSeeker
被创建,并且文件中的每一个字符都交给他去处理。
ClassDeclarationInfoSeeker.h
ClassDeclarationInfoSeeker
负责对每一个文件中的每一个字符进行处理,尝试找到和类声明相关的Token流。
#pragma once
#include<vector>
#include<string>
using namespace std;
//前向声明
class ClassInfoCollector;
class ClassDeclarationInfoSeeker //类声明信息的寻找者
{
private://私有成员
ClassInfoCollector* CollectorOwner; //所有者
vector<string> FoundTokens; //已经得到的Token
string PresentIdentifier; //当前标识符
public://接口
//构造函数
ClassDeclarationInfoSeeker(ClassInfoCollector* InCollector);
//处理下一个字符
void ProcessNextOneChar(char c);
private://内部调用
//得到一个token
void PutToken(string token);
//结束token流的读取
void EndTokenList();
//尝试插入一条类的信息,返回是否成功
bool TryInsertClassInfo();
};
ClassDeclarationInfoSeeker.cpp
ClassDeclarationInfoSeeker
内函数的实现:
#include"ClassDeclarationInfoSeeker.h"
#include"ClassInfoCollector.h"
#include<iostream>
//是否是有效的标识符可用字符
static bool IsValidIdentifierChar(char c)
{
if (c == '_')
return true;
if ((c >= '0') && (c <= '9'))
return true;
if ((c >= 'a') && (c <= 'z'))
return true;
if ((c >= 'A') && (c <= 'Z'))
return true;
return false;
}
//构造函数
ClassDeclarationInfoSeeker::ClassDeclarationInfoSeeker(ClassInfoCollector* InCollector)
{
CollectorOwner = InCollector;
}
//处理下一个字符
void ClassDeclarationInfoSeeker::ProcessNextOneChar(char c)
{
//是标识符:
if (IsValidIdentifierChar(c))
{
PresentIdentifier += c; //则PresentIdentifier添加字符
return;
}
//不是标识符:
if (PresentIdentifier.size() > 0) //如果PresentIdentifier有内容,
PutToken(PresentIdentifier); //则放置PresentIdentifier这个token
if ((c == ' ') || (c == '\n')) //如果是空格或者换行符
return; //什么也不做,继续看
if ((c == ':') || (c == ',')) //在寻找的字符
{
PutToken({ c }); //直接放置token
return;
}
//走到这里说明遇到一个不属于类声明中的字符,则结束token流的读取,
EndTokenList();
}
void ClassDeclarationInfoSeeker::PutToken(string token)
{
if (FoundTokens.size() == 0) //如果是第一个token,
{
if (token == "class") //则只在是class时添加
FoundTokens.push_back(token);
}
else //否则
FoundTokens.push_back(token); //直接添加
PresentIdentifier = "";//清空当前的标识符
}
void ClassDeclarationInfoSeeker::EndTokenList()
{
if(FoundTokens.size()>=2)//至少应为2
TryInsertClassInfo();
FoundTokens.clear();
PresentIdentifier = "";
}
bool ClassDeclarationInfoSeeker::TryInsertClassInfo()
{
ClassInfo info;
//冒号的位置,初始赋为token流长度
int colonPosition = FoundTokens.size();
//按顺序访问token
for (int i = 1; i < FoundTokens.size(); i++)
{
if (FoundTokens[i] == ":")//是冒号
{
colonPosition = i;
break;
}
else
info.ClassName = FoundTokens[i];//将FoundTokens[i]作为名字,如果有后续非冒号token,则替换成下一个token
}
//遍历冒号后的token
for (int i = colonPosition+1; i < FoundTokens.size(); i++)
{
int afterColonIndex = i - colonPosition-1;//冒号后的顺序
if (afterColonIndex % 3 == 0)//第一个,应检查是不是public/private/protected中的一个
{
if ((FoundTokens[i] == "public") || (FoundTokens[i] == "private") || (FoundTokens[i] == "protected"))
continue;
else
return false;
}
else if (afterColonIndex % 3 == 1)//第二个,应是父类的名字
{
info.ParentClasses.push_back(FoundTokens[i]);
}
else if (afterColonIndex % 3 == 2)//第三个,应是逗号
{
if (FoundTokens[i] != ",")
return false;
}
}
CollectorOwner->InsertOneInfo(info);
return true;
}
main.cpp
递归方式获得一个文件夹内的所有文件,然后收集信息。
#include <iostream>
#include <fstream>
#include<io.h>
#include"ClassInfoCollector.h"
using namespace std;
//递归地方式找到所有的文件
void GetFilesInFolder_recursion(string folder, vector<string>& files)
{
_finddata_t fileInfo; //储存得到的文件信息
//找第一个
auto findResult = _findfirst((folder+"\\*").c_str(), &fileInfo);
if (findResult == -1)
{
_findclose(findResult);
return ;
}
//找之后的
do
{
string fileName = fileInfo.name;
if ((fileInfo.attrib == _A_NORMAL)||(fileInfo.attrib == _A_ARCH)) //是文件
{
files.push_back(folder+"/"+fileName);
}
else if (fileInfo.attrib == _A_SUBDIR)//是文件夹
{
if (fileName == ".") //跳过得到的这个路径
continue;
if (fileName == "..") //跳过得到的这个路径
continue;
GetFilesInFolder_recursion(folder+"/"+ fileName,files);
}
} while (_findnext(findResult, &fileInfo) == 0);
_findclose(findResult);
}
int main()
{
ClassInfoCollector Collector;
vector<string> files;
GetFilesInFolder_recursion("D:/Temp",files);
for (auto f : files)
Collector.Collect(f);
//输出:
for (auto relationship : Collector.GetAllClassInheritRelationship())
cout << relationship.parentClass.c_str() << "-->" << relationship.childClass.c_str() << endl;
}
使用:
一个简单的测试
对象:
class ClassA
{
};
class ClassB : public ClassA
{
};
class ClassC:public ClassA
{
};
class ClassD:public ClassB
{
};
运行程序后输出:
ClassA-->ClassB
ClassA-->ClassC
ClassB-->ClassD
将其放到Mermaid中生成图:
测试2
测试虚幻4引擎下的\Engine\Source\Developer\DerivedDataCache
文件夹:
缺陷之处
毕竟,不是严谨的使用Clang等对代码进行分析,所以想要达到最严谨的判断还是需要更多的努力的,现在并没有保证绝对的严谨,至少没有考虑下面的情况:
- 没有考虑类声明中的模板
- 没有考虑类声明中有注释
附录:尝试运行Clang
1.下载安装
在 下载页面下载:
安装时勾选加入环境变量PATH
。
2.尝试
创建一个cpp代码D:/Temp/main.cpp
,内容如下:
class ClassA
{
};
class ClassB : public ClassA
{
};
然后尝试输出Token流:
clang -fmodules -fsyntax-only -Xclang -dump-tokens D:/Temp/main.cpp
:
尝试输出抽象语法树 AST:
clang -fmodules -fsyntax-only -Xclang -ast-dump D:/Temp/main.cpp