为代码中的类继承关系生成Mermaid图

想法

我一直认为,将代码中的内容以图形化的方式表示出来有助于理解。对于我来说,类之间的继承关系和模块之间的依赖关系是我比较关心的内容。不过,当项目中的代码很多时,手动为感兴趣的内容画图将会消耗很多时间,我觉得需要自动化的工具生成图形。

生成图形方面,可以借助 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中生成图:

ClassA
ClassB
ClassC
ClassD
测试2

测试虚幻4引擎下的\Engine\Source\Developer\DerivedDataCache文件夹:

FDerivedDataBackendInterface
FDerivedDataBackendAsyncPutWrapper
FDerivedDataBackendCorruptionWrapper
FDerivedDataBackend
FDerivedDataBackendGraph
FDerivedDataBackendVerifyWrapper
FDerivedDataCacheInterface
FDerivedDataCache
FNonAbandonableTask
FBuildAsyncWorker
IDerivedDataRollup
FDerivedDataRollup
FDerivedDataCacheWithRollups
IDerivedDataCacheModule
FDerivedDataCacheModule
FDerivedDataLimitKeyLengthWrapper
FFileSystemDerivedDataBackend
FHierarchicalDerivedDataBackend
FMemoryDerivedDataBackend
FPakFileDerivedDataBackend
FCompressedPakFileDerivedDataBackend
FRunnable
FDDCCleanup
IModuleInterface
MyBackend
IDDCUtilsModuleInterface

缺陷之处

毕竟,不是严谨的使用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
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值