Scala代码静态检查利器

大数据快速发展,催生以Spark等数据处理组件和技术同时,也让scala成为大数据领域炙手可热明星语言。与之热度形成反差的是代码检查和分析工具远远落后于Java, C/C++等老牌语言。

基于不同原理设计的代码检查工具有多种。Java代码检查工具有Findbugs、PMD、Checkstyle、Sonar。虽然Scala运行在JVM上,但是以上工具不能很好的兼容Scala检查。Scalastyle 是专门针对Scala代码而开发静态检查工具。本文介绍scalastyle使用,静态检查功能等。

本文内容包括一下几部分,略长,使用者可直接到第二部分:

  • 静态检查简介
  • scalastyle配置和使用
  • 源码分析

静态检查简介


在静态代码检查领域,有各种明星检查工具。代码检查的原理,参见[1],概括而言原理是:

  • 检查Java代码,缺陷模式匹配
  • 检查编译函数字节码

前者适用与Java代码分析,不适用于Scala源代码。缺点是针对代码的静态检查,运行时绑定和资源的操作无法进行检查。后者则为对编译完成的字节码进行检查。这种模式为基于JVM虚拟机运行Scala语言,提供了一种检查工具,比如Findbugs是对字节码的检查。两种检查模式优缺点并存。本文不讨论优缺点,仅关注Scalastyle使用和运行。

当前Scalastyle 1.0版本包含 69个检查项。检查规则文件可参考:scalastyle config
举例来讲静态检查如下:

//BAD
class TestClass{ // 类名后需要加空格
  def testFunc(in: Set[String]) = { // 非private接口需要 提供返回类型
    println("testFunc out")
  }
}
//GOOD
class TestClass {
  def testFunc(in: Set[String]):Unit = {
    println("testFunc out")
  }
}

可以看到针对Scala的源代码检查有助于代码规范化和可读性。对代码进行了强制性规范,保证开发任务代码格式上做到统一。

配置和使用


scalastyle config链接中配置项目很多,但非常易用,举例说明下:

以上设置激活了检查行结束是否有空格、检查文件长度 两项检查。如果想关闭,设置enable为false即可。根据项目需要设置检查规则。

以IDEA 开发工具为例说明:
IDEA环境本身已经支持scalastyle,不需要下载插件,仅需要配置。

若代码已经生成了IDEA项目,会存在.idea目录。从官方下载配置文件,将该配置文件复制到.idea目录下去或者放到工程项目的project目录。也可以参考:

Scalastyle examines your Scala code and indicates potential problems
with it. Place scalastyle_config.xml in the /.idea or
/project directory. Full documentation is available on the
Scalastyle website.

如下操作打开IDEA对scalastyle支持的开关:
selecting Settings->Editor->Inspections, then searching for Scala style inspections. 确保“scala style inspection”被勾中。

到这里完成了配置。Scalastyle会在我们编写代码时,准实时提示。我们根据提示把代码规范化。

源码分析


Scalastyle静态检查运行过程: 配置加载 -> 执行类型校验 -> 结果输出.

  • 1.配置加载

上文配置xml列举scalastyle格式检查类型,根据配置,IDEA加载xml文件检查项到内存中。实现加载类如下图ScalastyleConfiguration同时也实现了配置结构化读取、以及配置创建。

Scala代码静态检查利器_第1张图片
ScalastyleConfiguration类图

下面以Scalastyle配置文件,说明加载类运行过程。

Scala代码静态检查利器_第2张图片
XML配置示例

内容保存在 case class ScalastyleConfiguration中,结构如下:

case class ScalastyleConfiguration(name: String, commentFilter: Boolean, checks: List[ConfigurationChecker])

样例类各字段和XML关系显而易见。而关键 checkS保存类型检查的入口(图中class字符串),通过Java的类加载机制加载运行。
这也就意味着所有检查类别通过单例类ScalastyleConfiguration获取。在对代码进行检查时,只需调用ScalastyleConfiguration就可以完成。在这里我们看到: 接口设计在满足功能的前提,易读最优先保证。

  • 2.执行类型校验

Scalastyle类型检查是由工具类调用各个静态检查项目。工具类作为通用类,它是所有调用所有检查项的入口。而各静态检查项实现检查逻辑不管调用流程,调用和检查逻辑分离。

Scala代码静态检查利器_第3张图片
工具类

我们以ClassTypeParameterChecker为例来说明调用和检查过程。UML中,CheckUtils入参是classLoader : Option[ClassLoader]配置XML中class的字符值,负责加载检查项目类。 verifySource verifyFile分别基于scala文件和source类型检查。
verifySource根据模式匹配进行调用。先对匹配项有些印象,后面说明。
c match { case c: FileChecker => { c.verify(file, c.level, lines, lines) } case c: ScalariformChecker => { c.verify(file, c.level, scalariformAst.ast, lines) } case c: CombinedChecker =>{ c.verify(file, c.level, CombinedAst(scalariformAst.ast, lines), lines) } case _ => Nil }
FileSpec是检查结果输出系统待后面描述。其它实现和使用不赘述。

ClassTypeParameterCheckerUML涉及图。
[图片上传失败...(image-9b363-1516091666310)]
图中顶层类是Checker[A]。它属于所有检查项目的父类。checker子类包含ScalariformChecker子类。 名字有点熟悉吧,在工具类中verifySource匹配到的模式,会调用 verify[T <: FileSpec](file : T, level : Level, ast : A, lines : Lines) : List[Message[T]]方法。

  def verify[T <: FileSpec](file: T, level: Level, ast: A, lines: Lines): List[Message[T]] = {
verify(ast).map(p => toStyleError(file, p, level, lines))

}
该方法调用接口函数 verify(ast : A) : List[PositionError]。这个接口的实现在AbstractClassChecker,它的实现方法。

def verify(ast: CompilationUnit): List[PositionError] = {
val it = for {
  f <- visit[TmplDef, TmplClazz](map)(ast.immediateChildren.head)
  t <- traverse(f, matches)
} yield {
  PositionError(t.t.name.offset)
}
it

}
方法中调用待实现接口matches(t : TmplClazz) : boolean,在类型检查ClassTypeParameterChecker类中函数被实现,实现简单不赘述。也就是说只要对类类型参数检查,需要实现matcher就完成开发,对开发者而言,接口越简单,扩展性越强
让我们看下另一个函数matches(t : TypeParamClause) : boolean。它是实现class type 参数检查关键。

def matches(t: TypeParamClause): Boolean = {
val regexString = getString("regex", DefaultRegex)
val regex = regexString.r //class para 检查正则表达式

t.contents.flatMap(c => innermostName(c)).exists(s => !matchesRegex(regex, s))
}

函数中flatMap循环对数据每行进行处理。innermostName则是内容提取函数,然后和正则表达式匹配返回结果。

def innermostName(ast: Any): Option[String] = {
ast match {
  case typeParam: TypeParam => {
    typeParam.contents match {
      case List(GeneralTokens(list)) => Some(list.head.text)
      case List(GeneralTokens(list), TypeParamClause(x)) => innermostName(x(1))
      case VarianceTypeElement(_) :: GeneralTokens(list) :: Nil => Some(list.head.text)
      case GeneralTokens(list) :: tail => Some(list.head.text)
      case VarianceTypeElement(_) :: GeneralTokens(list) :: tail => Some(list.head.text)
      case _ => None
    }
  }
  case _ => None
}


到这里,整个调用流程已经完成。如果我们开发新的检查项目,只需要实现matcher函数接口。对我们而言,一个的项目,提供这样接口在易读性、可测试性无疑是必须的。

  • 3.结果输出

经过代码分析,在检查校验时def verify[T <: FileSpec](file: T, level: Level, ast: A, lines: Lines): List[Message[T]]被调用,所有List[Message[T]]是我们要说明结果输出。
Message的定义sealed abstract class Message[+T <: FileSpec](),T类型是 FileSpec协变类型。那么T对应哪些类型哪?

trait FileSpec
class RealFileSpec(val name: String, val encoding: Option[String]) extends FileSpec
class SourceSpec(val name: String, val contents: String) extends FileSpec

FileSpec就是榨干的空壳,内容都RealFileSpec等子类存储。因此说输出结果就是多个class存储返回结果。

写在后面####


Scalastyle静态类型检查是开发者的福音,是一款订制化的检查工具。从另一方面说:基于开发的原理造成先天的缺陷,它又略尴尬。因为它只提供代码格式检查,类似findbugs 资源关闭,空指针检查等并不具备。

使用Scalastyle时常用关键字://scalastyle:on //scalastyle:off ,懒人的大杀器。

注:文中UML使用是基于Java的工具画的,存在不准确情况。只是描述基本的类结构。


1.https://www.ibm.com/developerworks/cn/java/j-lo-statictest-tools/

你可能感兴趣的:(Scala代码静态检查利器)