自2017年Google I/O 大会上,谷歌宣布Android 平台正式支持Kotlin之后,Kotlin发展神速,近两年,kotlin已跻身Stack Overflow上最受喜爱的语言之一,也是GitHub上贡献者数量增长最快的语言之一,现在Kotlin编程语言是Android应用程序开发人员的首选语言,并且Google许多新增的Jetpack API和功能也优先提供Kotlin版本。在使用Kotlin语言编程的时候,代码规范相信每个开发人员或多或少都会遇到和思考的一个问题,而本文主要介绍的是一款基于IDE对Kotlin语言进行实时代码扫描的插件。
在2018年中开始,珍爱网相关新生项目和旧项目的新生业务都开始使用kotlin语言进行开发了,众所周知,Kotlin有着空指针安全,方法扩展,支持函数式编程等诸多特性,这使得Kotlin比Java更加简洁优雅,代码可读性更高,这也大大提高了我们的开发效率,但是在使用中也会发现,使用不当也会存在一定的性能的开销,加上大部分开发人员都是由Java转Kotlin开发的,所以更加容易犯一些低级的错误,例如下面的伴生对象:
class Test {
companion object {
val A = "Hello"
}
}
public class Client {
public void say(){
LogUtils.d(Test.Companion.getA());
}
}
虽然上面的代码简洁明了,但是编译成如下Java代码之后就不是那么简洁了
public final class Test {
private static final String A = "Hello";
public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null);
(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0003\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"},
d2 = {"Lcom/za/consultation/Test$Companion;", "", "()V", "A", "", "getA", "()Ljava/lang/String;", "app"}
)
public static final class Companion {
public final String getA() {
return Test.A;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
据我们了解,目前外面开源的对Kotlin进行静态代码分析的工具主要有以下:
Android Lint是Google出品的,针对Android项目进行代码进行扫描的工具,功能非常强大,不仅仅能够对Kotlin语言的支持,还能对Java,静态资源文件,Xml文件等进行扫描,由于我们这个主要是针对Kotlin,而且想做成一个单独的IDE插件,前期也试过使用Android Lint,但是发现做成的IDE包很大,和里面Lint使用了很多IDE Core的包,不利于自己改造,所以放弃了这个方案
DeteKt是Github上一个很火的,针对Kotlin进行代码检查的插件,而且已经内嵌很多实用的规则,这边也尝试集成过,但是发现这个库的自定义规则比较麻烦和只支持控制台输出,不方便阅读修改,同时开发人员对错误修复之后再去扫描没有实时生效,最终也就放弃了对其改造
KLint更多是对Kotlin代码风格检查的工具,不是很适合我们这个需求定位,需要大量改造才能使用
基于以上现有的工具和插件的局限,为了以后的拓展,所以我们决定自主开发一款具有实时检查Kotlin代码规范并且实时提醒的IDE插件。
要实现一个款结合IDE对Kotlin代码进行实时提醒的插件需要掌握下面的知识:
IDE插件开发基本流程
AST,PSI等知识
Kotlin代码语法树的解析
当然本文主要是讲如何对Kotlin文件进行代码分析,所以相关的IDE插件开发的基本流程在这里不多说了,大家可以网上学习一下IDE插件开发的流程(参考该教程[1]),下面主要讲一下 AST,PSI和Kotlin代码语法树的解析
AST(Abstract Syntax Tree)即为"抽象语法树",是编辑器对代码的第一步加工之后的结果,以一个树的形式表示源代码,例如下面的一段java代 码转成AST是这样的
如上图右边是Java代码解析成AST之后的树形结构,其实上面提到的静态代码扫描工具Android Lint 第一次引入的时候也是基于Lombok-Ast作为自 己的AST Parser,但是后来由于Lombok-Ast缓慢跟不上发展,所以在Lint 25.2.0版增加了IntelliJ的PSI作为新的AST Parser
为什么这里要介绍一下PSI呢,因为在后面插件内部很多地方都是通过PSI进行转换的,上面Kotlin代码解析提到是通过Kotlin-Compiler- Embeddable这个库把Kotlin文件解析成PSI的,那么PSI是什么呢?PSI(Program Structure Interface) 文件是表示一个文件的内容作为在一个特定的编程语言元素的层次结构的结构的根。在IDE上我们可以通过安装PsiViewer来看看Kotlin对应的Psi是什么样的。
(2) PsiViewer对Kotlin文件解析
从上图我们可以看到,左边1部分是原始的Kotlin文件,右上角2部分是Kotlin文件通过PsiViewer转换成PSI后的效果,从图中可以看出,转成的PSI的层次结构是一个很有规则的树状结构,这让我们想到来抽象语法树,而我们这个IDE kotlin静态代码实时提醒正是先把Kotlin文件专场PSI然后再通过遍历PSI专场自己定义好的语法树Tree的过程。
我们都知道,Java的静态代码扫描原理是通过把Java文件生产抽象 语法树(AST),然后遍历语法树进行代码规则检查,同样的道理,Kotlin也有自己的抽象语法树,只可惜现在目前还没有一个单独解析kotlin成抽象语法树的库,本插件用的是Jetbrains提供的Kotlin-Compiler-Embeddable库,通过这个库先将Kotlin文件解释成PSI,然后再转换成自己的定义的树状结构(AST),然后去适配自己的规则。所以整个解析流程如下图:
在具体介绍插件之前,先来了解一下整个插件的大致的工作流
插件安装后重启IDE,此时会加载本插件,插件进行启动,插件启动后会加载上图的ZhenaiLocalInspectionToolProvider,ZhenaiLocalInspectionToolProvider主要负责以下功能:
(1)加载启用的规则Rules
(2)注册对应规则的实例DelegateKotlinInspection给IDE,注册 成功之后你会在IDE面板上看到下图的界面的话代表已经成功注册:
DelegateKotlinInspection注册成功之后,那么当Kotlin文件触发之后IDE就会执行注册的DelegateKotlinInspection,
然后DelegateKotlinInspection调用ZhenaiKotlinInspection,
ZhenaiKotlinInspection再调用ZhenaiKotlinInspectionInvoker进行Kotlin文件检查,
ZhenaiKotlinInspectionInvoker会通过调用KotlinConverter将Kotlin文件转成对应的抽象语法树AST,然后调用ChecksVisitor进行树节点遍历去匹配相对于的规则,最后返回错误结果Problems
如上图所示,为了以后扩展和维护,对整个插件进行以下分层:
Plugin层是提供给开发人员进行代码扫描的入口,方便不同编辑器定制不同的操作界面和展示界面,上图是IDE的相关界面:1是代码扫描的操作界面,2是代码检测实时提醒界面,3是代码扫描后结果的展示界面,目前的话只开发了支持Android Studio的插件
Client层是为Plugin提供相关Api的调用,里面包含了静态代码规则(ChecksVisitor)扫描的执行和错误结果(Problems)的获取,,并将结果给Plugin层展示
这里主要是规则相关的开发,规则集主要包含以下几大类:
注释类规则集
例如:UndocumentedInterfaceOrAbstractFunction
完善的文档注释能够让使用者知道某个类或者某个方法的用户,尤其接口方法和抽象方法都是供外部继承或者调用的,那么接口方法或者抽象方法就应该添加注释文档,说明用途,方便外部调用了解
interface TestClass{
fun test()
}
合规范的写法:
interface TestClass{
/**
* 测试方法
*/
fun test()
}
复杂性规则集
例如:LongParameterList
方法参数过多不仅仅影响代码可读性,同时增加了该方法调用的难度,复杂度,所以对于参数过多的方法应该组装成一个对象,让外部调用
threshold(默认值:6)
不合规范的写法:
class TestImpl {
fun sayHello(age:String,name:String,weight:Int,height:Int,salary:Float,sex:Int){
}
}
合规范的写法:
class TestImpl {
fun sayHello(personal:Personal){
}
}
空块规则集
EmptyFunctionBlock
空代码块没有任何的具体实现,这样的方法是无用的,应该及时清理
不合规范的写法:
fun sayHello(){
//is empty,remove
性能规则集
companion object常量和变量的定义
伴生对象内部常量使用不当会伴随着很多get的方法诞生,编 译成java代码后调用一个常量需要调用很多步才能真正获取得到
不合规范的写法:
companion object {
var a = 1
val b = "2"
val c = 3f
}
合规范的写法:
companion object {
var a = 1
val b = "2"
const val c = 3f
}
API调用规范集
Log日志打印
相信每个公司都有自己封装好的通用的库供应到不同的业务统一调用。而日志在平时开发中占着一个很重要的位置,Log日志打印规范能够很好帮助开发人员对问题排查显得更加容易方便,本公司也有自己的日志打印库,所以平时涉及相关日志打印不建议使用Android原生的。
不合规范的写法:
fun doPay() {
Log.d(TAG,"doPay------------")
}
合规范的写法:
fun doPay() {
LogUtils.d(TAG,"doPlay--------------")
}
core层时整个插件的核心,里面包含了几个很重要的类,下面介绍一下:
(1) 接口Tree
前面有提到,此插件首先是通过kotlin-compiler-embeddable将kotlin文件解析成PSI然后再转成自定义好的语法树(tree),而Tree是所有树节点的父类,后续相关方法树(FunTree,CompanionObjectTree等)都是实现来Tree。
(2) KotlinConverter 类
KotlinConverter类核心功能是调用kotlin-compiler-embeddable包将kotlin语言文件解析成的Psi
/**
* 将kotlin 文件转成PSI
* @return
*/
private static PsiFileFactory psiFileFactory() {
CoreFileTypeRegistry fileTypeRegistry = new CoreFileTypeRegistry();
fileTypeRegistry.registerFileType(KotlinFileType.INSTANCE, "kt");
FileTypeRegistry.ourInstanceGetter = new StaticGetter<>(fileTypeRegistry);
Disposable disposable = Disposer.newDisposable();
MockApplication application = new MockApplication(disposable);
FileDocumentManager fileDocMgr = new MockFileDocumentManagerImpl(DocumentImpl::new, null);
application.registerService(FileDocumentManager.class, fileDocMgr);
PsiBuilderFactoryImpl psiBuilderFactory = new PsiBuilderFactoryImpl();
application.registerService(PsiBuilderFactory.class, psiBuilderFactory);
application.registerService(ProgressManager.class, new CoreProgressManager());
ApplicationManager.setApplication(application, FileTypeRegistry.ourInstanceGetter, disposable);
Extensions.getArea(null).registerExtensionPoint(MetaLanguage.EP_NAME.getName(), MetaLanguage.class.getName(), ExtensionPoint.Kind.INTERFACE);
Extensions.registerAreaClass("IDEA_PROJECT", null);
MockProject project = new MockProject(null, disposable);
project.registerService(ScriptDefinitionProvider.class, CliScriptDefinitionProvider.class);
LanguageParserDefinitions.INSTANCE.addExplicitExtension(KotlinLanguage.INSTANCE, new KotlinParserDefinition());
CoreASTFactory astFactory = new CoreASTFactory();
LanguageASTFactory.INSTANCE.addExplicitExtension(KotlinLanguage.INSTANCE, astFactory);
LanguageASTFactory.INSTANCE.addExplicitExtension(Language.ANY, astFactory);
PsiManager psiManager = new PsiManagerImpl(project, fileDocMgr, psiBuilderFactory, null, null, null);
return new PsiFileFactoryImpl(psiManager);
}
(3) KotlinTreeVisitor 类
KotlinTreeVisitor是对转换后的PSI转成定义好AST对应的相关树节点
/**
* 将PSI 转成 Tree
*
* @param element
* @param metaData
* @return
*/
private Tree convertElementToAST(PsiElement element, TreeMetaData metaData) {
int psiType = getElementType(element);
switch (psiType) {
case PsiElementType.TYPE_KTOPERATIONEXPRESSION:
return createOperationExpression(metaData, (KtOperationExpression) element);
case PsiElementType.TYPE_KTNAMEREFERENCEEXPRESSION:
return createIdentifierTree(metaData, element.getText());
case PsiElementType.TYPE_KTBLOCKEXPRESSION:
List<Tree> statementOrExpressions = list(((KtBlockExpression) element).getStatements().stream());
return new BlockTreeImpl(metaData, statementOrExpressions);
......
......
case PsiElementType.TYPE_KTRETURNEXPRESSION:
return createReturnTree(metaData, (KtReturnExpression) element);
case PsiElementType.TYPE_KTTHROWEXPRESSION:
return createThrowTree(metaData, (KtThrowExpression) element);
case PsiElementType.TYPE_KTOBJECTDECLARATION:
return createObjectDeclarationTree(metaData, (KtObjectDeclaration) element);
default:
return convertElementToNative(element, metaData);
}
}
这里是使用Jetbrains提供的kotlin-compiler-embeddable包,将 Kotlin文件转成PSI文件结构
本插件内置的Kotlin规则虽然已经涵盖了一些比较普遍的一些代码检查,但是在我们实际开发中可能还会存在以下问题:
(1)在不同的团队或者不同的公司肯定存在一些个性化需求,而本插件内置的一些大部分都是一些公共的问题,需要定制化的话就需要能够实现自定义规则的编写
(2)本插件开启的一些规则可能也不适合别的团队或者公司 基于以上原因,在项目中经常需要用到规则的自定义
那么自定义KotlinLint规则需要哪些步骤呢,其实在做本插件的时候已经考虑到了后面的规则扩展和自定义,所以提供了一套方便外部实现的接口,总结一下大概流程(如下图)1.编写规则相关信息2.编写规则检查的实际逻辑3.规则的配置与使用
下面来看看具体的代码实现
/**
* 方法参数过多规则
*/
class TooManyParametersCheck : ICheck {
/**
* 最大参数个数
*/
private val DEFAULT_MAX = 7
var max = DEFAULT_MAX
//编写规则信息
private val sIssue = SIssue.SIssueBuilder()
.name("方法参数过多检查")//规则名称
.issueId("TooManyParametersCheck")//规则ID
.des("方法参数过多,超过" + DEFAULT_MAX + "个").build()//规则描述
override fun initialize(init: InitContext) {
//注册监听的FunctionDeclarationTree树
init.register(FunctionDeclarationTree::class.java, BiConsumer { ctx, tree ->
if (!tree.isConstructor && !isOverrideMethod(tree) && tree.formalParameters().size > max) {
//匹配到进行错误结果的上报
if (tree.name() == null) {
ctx.reportIssue(tree, sIssue)
} else {
ctx.reportIssue(tree.name()!!, sIssue)
}
}
})
}
override fun getSIssue(): SIssue {
return sIssue
}
/**
* 是否是重载方法
*
* @param tree
* @return
*/
private fun isOverrideMethod(tree: FunctionDeclarationTree): Boolean {
return tree.modifiers().stream().anyMatch { mod ->
if (mod !is ModifierTree) {
return@anyMatch false
}
mod.kind() == ModifierTree.Kind.OVERRIDE
}
}
}
如上,添加一个规则先要继承ICheck类:
/**
* 规则的统一接口
*/
interface ICheck {
/**
* 初始化
*
* @param init
*/
void initialize(InitContext init);
/**
* 获取错误规则描述
*
* @return
*/
SIssue getSIssue();
}
实现initialize方法,然后通过init注册自己需要监听的Tree类型,上面的规则是方法参数过多的问题,所以注册的FunctionDeclarationTree方法树,然后获取方法参数的个数,匹配到就report错误
通过Jetbrains官方仓库安装
打开Settings >> Plugins >> Marketplace
在搜索框输入Zhenai 便可以看到Zhenai Android Coding Guidelines插件了,然后点击安装重启后生效 注意:因为插件zip包托管在 Jetbrains官方CDN上,所以是从国外的服务器进行下载,可能会出现超时的情况。
通过下载安装包进行安装
扫描单个文件
扫描整个项目
代码如上图,勾选before commit中的Zhenai Code Guidelines之后,提交的时候 如果发现提交的代码有不符合代码规范的会弹出提醒。
添加IDE实时提醒插件之后,最终形成了上图的一套完整的静态代码扫描流程:
[1]
可参考该网上教程: http://https://blog.csdn.net/qq_36838191/article/details/82978693