使用 IBM Rational Software Analyzer 进行静态分析,第 4 部分: 集成您自己的分析工具

本系列静态分析四篇文章的最后一篇,描述了怎样通过使用 Rational® Software Analyzer 框架提供的 API,来在分析用户界面中,集成一个新的分析引擎。

Steve Gutz, 软件经理, IBM

Steve Gutz 是负责 IBM Rational 代码分析工具的开发经理。除了构架并管理 IBM 的商业产品以外,Steve 过去还致力于 Eclipse Test and Performance Tools project(TPTP),专注于基本的代码审查工具的实现和集成中的改进。在 2002 年加入 IBM 渥太华实验室之前,他在许多公共和私有的公司中担任高级管理和执行职位,包括两个他自己的成功创业。他还写过两本书和许多文章,并且经常在会议上演讲。



2008 年 11 月 07 日

IBM® Rational® Software Analyzer 提倡并简化了自动代码质量改进过程。它最初被设计成一个 API 和用户界面,以在其他 Rational 产品中创建并集成静态分析工具,例如,这些 Rational 产品有 Rational® Application Developer 以及 Rational® Software Architect。IBM Rational Software Analyzer 现在已经发展成为一个完整的,能让软件开发员们在他们每天的开发过程中,轻松进行自动代码评审以及结构分析的标准 Provider 。

您将学到的内容

在本系列文章的第 1,2,3 部分,我们已经检查了,用于静态分析的一般用户界面,同样还有执行 Rational Software Analyzer 提供的,Java 代码评审分析 Provider 新规则所需的步骤。到目前为止,关于分析 API 的大多数方面,都最大限度的对您进行了隐藏 ,但是如果您正在阅读本篇文章,那么您必须向工作台,集成一些新形式的分析工具。为了做到这点,您需要再大大深入一些。

在本部分中,我们将进行集成新形式分析工具的过程。其中没有什么太困难的地方,但是,它所涉及到的内容,当然要比添加一个 Java 代码评审规则多得多。当完成本篇文章的学习后,您将从头至尾创建一个新的分析 Provider,并将该 Provider 严密集成到 Rational Software Analyzer 分析框架中去。其中所有的代码以及程序,同样适用于 Rational 建模工具产品的集成:Rational Software Architect,Rational Application Developer,Rational® Software Developer 以及 Rational® Software Modeler。

架构概述

在前面的文章中,我们省略了分析结构的大多数实际细节,这样我们就可以将注意力,专注的放在,用于执行 Java 代码评审规则的某些特定方面上。但是从本篇文章的目的出发,我们将不可避免地考虑,分析框架是怎样实际运行的。同我们在本系列文章中讨论的大多数事情一样,我们尽量减少您阅读理解时的障碍,这样您就可以快速并轻松地掌握文章中涉及到的概念。

接口层次性

理解分析框架的第一步,是查看分析框架中涉及到的元素。图 1 中的图表包含了,创建及运行分析 Provider 所需的多数类以及接口。

图 1. 类和接口图
图表的图
表 1:类以及接口的描述
类或者接口描述
AbstractAnalysisElement所有分析元素的基本类。您可能从来没有直接使用过它,但是它定义了所有分析元素过程的特征。
AbstractAnalysisProvider一个分析 Provider 是个定义一种静态分析工具的元素。另外,Provider 拥有一个或多个目录。例如,Java 代码评审就是一个分析 Provider。
DefaultAnalysisCategory一个类别就是一个对亚类别以及规则进行分类的群体概念,它基于一个公共特征。例如,一个 Threading 类别包含了一系列的与线程相关的规则 。
AbstractAnalysisRule规则就是分析的凭据。正如您在本系列前面文章中看到的那样,规则接受一个或多个分析资源并进行演化。基于演化的结果,可以创建结果 。
AbstractAnalysisResult结果包含的关于一系列资源规则的结果的信息,得以分析。包含特定领域数据的结果,必须由观察者提供。结果并不与规则直接联系,而是由分析历史所拥有(下面将讨论到)。

历史框架

如果您已使用了 Rational Software Analyzer 提供的代码评审特征,那么每次当它运行时,在分析结果视图中都会创建一个新的历史浏览条目。该条目包括了所有 Provider,类别以及在分析启动配置中定义的规则的列表,还有选择用于分析的资源的列表。最后,它还包含了在分析过程中产生的结果的列表 。这一系列的信息集中到一起,就叫做历史,它类似于图 2。

图 2. 历史框架
工作区域的图

如果您更进一步地评审与历史有关的结构(见图 3),您将会注意到类 AbstractAnalysisRule 使用 AnalysisHistory 来储存所有产生的结果。当分析过程开始时,AnalysisHistoryFactory 开始创建历史,该实例还适用于当前过程中涉及到的分析元素的列表 (主要是被选择规则的列表)。当您创建一个新的分析 Provider 时,它怎样运行并不重要,知道分析开始时,您的 Provider 将接受该实例就足够了。

图 3. 分析元素的图表
图表的图

基本过程序列

当用户选择一系列的规则时,他们并没有意识到,他们同时也选择了这些规则拥有的类别以及 Provider。当有人点击 Analyze 按钮,就会创建一个新的历史实例,并且被选择 Provider,类别以及规则的列表,在分析过程开始前就会被联系上。

在分析框架内部,有一个 AnalysisProviderManager 类,它管理着系统中所有的 Provider。当用户开始分析工作区的资源时,该类开始为每一个有功能的 Provider 进行一项异步操作,这称作一个 analyze() 方法。

一般地,每个 Provider 的 analyze() 方法简单地通过它的类别进行重复,并访问它们相应的 analyze() 方法。反过来,每个类别引用它所包含任何规则的 analyze() 方法。然后规则进行一些操作并产生结果,同时产生的,还有评审结果所需的所有特定领域的数据。所有的结果都存储在历史实例中,后者是在分析开始时被创建的 。

您可以看到,这是一个非常简单的过程,但是它已经足够进行任何种类的静态分析。我们已经涉及到了基础工作,这已足够开始创建一个新 Provider 了。

从第一准则创建一个 Provider

在本段中,您将从底层开始创建一个新的分析 Provider 。如果您想为某个特定的领域,创建一个完全崭新的分析回路的话,您可以按照这个精确的过程进行。完成该项的过程几乎是琐碎的,因为您只需要创建一个 Provider 类,一个结果类,以及一个扩展。

注意:
如果您还没有为您的语言领域准备一个已完成的剖析器时,例如 Java 开发工具或者 Eclipse C/C++ Development Tools(CDT),您将需要创建一个 。 Java CC 或者 ANTLR 是完成此项任务的典型工具,但是从本文的目的出发,它们并不是必需的。

创建 Provider 插件

在本例中,您将简单地重复 Rational Software Analyzer 中 C/C++ 分析 Provider 的操作。我们需要的第一个条目是一个插件项目,我们的代码和扩展将放在这个插件项目中。

  1. 选择 File > New > Project 菜单选项,并在随后的条目中,选择 Plug-in Project
  2. 点击 Next,然后在 Names 区域输入 analysis.mycpp
  3. 再次点击 Next, 并输入表格数据(参见图 4):
图 4. 创建一个新插件项目
目录框的图
  1. 为了完成项目的创建,点击 Finish

为了创建分析工具,您需要向插件添加一些附属物。

  1. 在编辑器中打开 META-INF/MANIFEST.MF 文件,并选择 Dependencies 项。
  2. 在 Required Plug-ins 列表中,添加如图 5 所示的插件。
图 5. 添加插件
工作区的图

创建剖析器类

在创建任何种类的 Provider 之前,需要有一些特定领域的剖析器存在。例如,在 Rational Software Analyzer 的 Java 代码评审 Provider 中 ,需要有类 CodeReviewResource 来管理 Java 源文件中的所有查询。在该类中,不需要去创建一个剖析器,因为 Eclipse 已经为 JDT API 访问的 Java,提供了一个完整的 DOM(Document Object Model)。

对于 C 与 C++,我们可以同等的轻松,因为 Eclipse 同样提供了一个叫做 CDT (C/C++ Development Tooling)的开放源项目,它包含了一个目标语言的相似剖析器 。我们在这里并不深入讨论 CRT 的细节,但是您可以在 CDT site 找到所有您需要的,包括 API Javadocs。因为 CDT 的细节部分已经超出了本篇文章的讨论范围,您可以跳过这点。列表 1 所示的代码,是您剖析 C/C++ 代码时所需要使用的 CodeReviewResource 类。它基本上是一个简单的 API 和 CDT,从而简化了它与您的分析 Provider 之间的接口。

列表 1. CodeReviewResource 类
package analysis.mycpp;

import java.util.Iterator;
import java.util.List;

import org.eclipse.cdt.core.dom.CDOM;
import org.eclipse.cdt.core.dom.IASTServiceProvider.UnsupportedDialectException;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;

import com.ibm.rsaz.analysis.core.logging.Log;
import com.ibm.rsaz.analysis.core.rule.AbstractAnalysisRule;


public class CodeReviewResource {
private static CDOM dom = CDOM.getInstance();
private IResource resource;
private IASTTranslationUnit resourceCompUnit;


public CodeReviewResource( IResource resource ) { 
this.resource = resource; 
try {
resourceCompUnit = dom.getTranslationUnit( (IFile) resource );
} catch (UnsupportedDialectException e) {
Log.severe( "", e); //$NON-NLS-1$
}
}

public List getTypedNodeList( IASTNode node, int nodeType, boolean searchChildren ) {
CodeReviewVisitor visitor = new CodeReviewVisitor( nodeType, searchChildren );
node.accept( visitor ); 
return visitor.getAstNodeList();
}

public List getTypedNodeList( IASTNode node, int nodeType ) {
return getTypedNodeList( node, nodeType, true );
}

public List getTypedNodeList( IASTNode node, int[] nodeTypes, boolean searchChildren ) {
CodeReviewVisitor visitor = new CodeReviewVisitor( nodeTypes, searchChildren );
node.accept( visitor ); 
return visitor.getAstNodeList();
}

public List getTypedNodeList( IASTNode node, int[] nodeTypes ) {
return getTypedNodeList( node, nodeTypes, true );
}

public void generateResultsForASTNode(AbstractAnalysisRule rule, 
String historyId, IASTNode node ) {
// The location information for this node
IASTFileLocation loc = node.getFileLocation();

// If we get here create a result
CodeReviewResult result = new CodeReviewResult(
resource.getFullPath().toOSString(),
loc.getStartingLineNumber(), loc.getNodeOffset(),
loc.getNodeLength(), node, this );
result.setOwner( rule );

// Save the rule in the history
rule.addHistoryResultSet( historyId, result );
}

public boolean generateResultsForASTNodes(AbstractAnalysisRule rule, 
String historyId, List list ) {
// Crawl through the method invocations
boolean addedResult = false;
for( Iterator it = list.iterator(); it.hasNext(); ) {
generateResultsForASTNode( rule, historyId, (IASTNode)it.next() );
addedResult = true;
}
return addedResult;
}

public IResource getResource() {
return resource;
}

public IASTTranslationUnit getResourceCompUnit() {
return resourceCompUnit;
}
}

您可能会注意到参考 CodeReviewResource 是一种叫做 CodeReviewVisitor 的类。这是您需要创建以和 CDT 一起剖析 C/C++ 文件的另一个类。类似于 JDT, CDT DOM 使用一个访问者模板,以和它的剖析树状图交流。因为您有特定的需求,您就需要为您的需求设置一个新的访问器。同样,我们并不讨论该类的具体细节,但是您可以在 CDT Javadoc 中找到关于它的相关文件。列表 2 显示了该应用程序的 visitor 类。

列表 2. Visitor 类
package analysis.mycpp;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.IASTDeclSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTInitializer;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTParameterDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTProblem;
import org.eclipse.cdt.core.dom.ast.IASTStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IASTTypeId;
import org.eclipse.cdt.core.parser.ast.IASTEnumerator;

public class CodeReviewVisitor
extends ASTVisitor
{
public static int TYPE_IASTNode = -1;

public static int TYPE_IASTDeclaration = 12;
public static int TYPE_IASTDeclarator = 14;
public static int TYPE_IASTDeclSpecifier = 15;
public static int TYPE_IASTEnumerator = 21;
public static int TYPE_IASTExpression = 22;
public static int TYPE_IASTGotoStatement = 32;
public static int TYPE_IASTInitializer = 35;
public static int TYPE_IASTName = 40;
public static int TYPE_IASTParameterDeclaration = 43;
public static int TYPE_IASTProblem = 59;
public static int TYPE_IASTStatement = 68;
public static int TYPE_IASTTranslationUnit = 70;
public static int TYPE_IASTTypeId = 71;


private List astNodeList;
private int[] astNodeTypes;
private boolean searchChildren;

public CodeReviewVisitor( int nodeType )
{
astNodeTypes = new int[1];
astNodeTypes[0] = nodeType;
astNodeList = new ArrayList(10);
searchChildren = true;
init();
}

public CodeReviewVisitor( int nodeType, boolean searchChildren )
{
astNodeTypes = new int[1];
astNodeTypes[0] = nodeType;
astNodeList = new ArrayList(10);
this.searchChildren = searchChildren;
init();
}

public CodeReviewVisitor( int[] nodeTypes )
{
astNodeTypes = nodeTypes;
astNodeList = new ArrayList(10);
init();
}

public CodeReviewVisitor( int[] nodeTypes, boolean searchChildren )
{
astNodeTypes = nodeTypes;
astNodeList = new ArrayList(10);
this.searchChildren = searchChildren;
init();
}

private void init() {
shouldVisitNames = true;
shouldVisitDeclarations = true;
shouldVisitInitializers = true;
shouldVisitParameterDeclarations = true;
shouldVisitDeclarators = true;
shouldVisitDeclSpecifiers = true;
shouldVisitExpressions = true;
shouldVisitStatements = true;
shouldVisitTypeIds = true;
shouldVisitEnumerators = true;
shouldVisitTranslationUnit = true;
shouldVisitProblems = true;
}

public List getAstNodeList() {
return astNodeList;
}

private int visitAny( IASTNode node, int nodeTypee) {
for( int iCtr = 0; iCtr < astNodeTypes.length; iCtr++ ) {
if( astNodeTypes[iCtr] == nodeTypee ) {
astNodeList.add(node);
}
}

// Visit children if required
if( searchChildren ) {
return ASTVisitor.PROCESS_CONTINUE;
}
return ASTVisitor.PROCESS_SKIP;
}


public int visit(IASTTranslationUnit node) {
return visitAny( node, TYPE_IASTTranslationUnit);
}

public int visit(IASTName node) {
return visitAny( node, TYPE_IASTName);
}

public int visit(IASTDeclaration node) {
return visitAny( node, TYPE_IASTDeclaration);
}

public int visit(IASTInitializer node) {
return visitAny( node, TYPE_IASTInitializer);
}

public int visit(IASTParameterDeclaration node) {
return visitAny( node, TYPE_IASTParameterDeclaration);
}

public int visit(IASTDeclarator node) {
return visitAny( node, TYPE_IASTDeclarator);
}

public int visit(IASTDeclSpecifier node) {
return visitAny( node, TYPE_IASTDeclSpecifier);
}

public int visit(IASTExpression node) {
return visitAny( node, TYPE_IASTExpression);
}

public int visit(IASTStatement node) {
return visitAny( node, TYPE_IASTStatement);
}

public int visit(IASTTypeId node) {
return visitAny( node, TYPE_IASTTypeId);
}

public int visit(IASTEnumerator node) {
return visitAny( (IASTNode)node, TYPE_IASTEnumerator);
}

public int visit( IASTProblem node ){
return visitAny( node, TYPE_IASTProblem);
} 
}

如果您要使用 CDT,那么您将有一些新的需求:

首先,Eclipse 工作区必须安装 CDT 特征:

  1. 访问 <a href="http://www.eclipse.org/cdt" target="_blank">CDT 网站</a>,并按照上面的安装提示进行安装。

接下来,您需要向插件声明文件添加一个额外的附属物:

  1. 打开声明编辑器,并向所需的插件附属物列表添加 org.eclipse.cdt.core 插件。
  2. 现在您正在同资源协调工作,所以添加 org.eclipse.core.resources 插件以作为一个附属物。

注意到,这里显示的 CodeReviewResource 类是一些黑框。但是,正如您所看到的那样,您正在创建的 Provider 实际上与该类并不直接交流 。实际上,因为这是一个特定的,用于与某个领域剖析器协调工作的 API ,只有规则才会直接与该类相交流。 Provider 唯一的任务,是为分析的每一个资源创建新的剖析器,并将其传递给规则。

创建一个 Result 类

您可以开始创建 C++ 代码评审的最后一步,是定义您预期结果的外观。从 Provider 的角度看,一个分析结果基本上可以被描述成特定领域数据,以及管理这些数据一些基本访问方法的容器。对于一个 C/C++ 结果来说,您需要去定位结果产生的资源的位置,以及涉及代码的起始位置。因为您正在创建的 Provider 使用 CDT,您就可以同样追踪 CDT Abstract Syntax Tree (AST)节点,这样如果需要的话,您就可以找到确切的位置。列表 3 中的代码定义了,您将用于呈现 C/C++ 结果的类。

列表 3. 呈现 C/C++ 结果的类
package analysis.mycpp;

import java.io.File;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import com.ibm.rsaz.analysis.core.result.AbstractAnalysisResult;

public class CodeReviewResult
extends AbstractAnalysisResult 
{

private String resourceName = null;
private int lineNumber;
private int startPositionSelection;
private int lengthSelection;
private CodeReviewResource resource = null;
private IASTNode myNode = null;

private static final char LINE_SEP = ':'; 

public CodeReviewResult(String resName, int p_lineNumber, 
int p_startPosSelection, int p_lengthSelection, 
IASTNode node, CodeReviewResource resource){ 
super();

this.resourceName = resName;
this.lineNumber = p_lineNumber;
this.startPositionSelection = p_startPosSelection;
this.lengthSelection = p_lengthSelection;

this.resource = resource;
this.myNode = node;
}


/**
* Overrides the parent class to provide a custom label for
* code review results.
* 
* @return The label string for this result
*/
public String getLabel() { 

String label = super.getLabel();

if( label == null || label.length() == 0 ) {

String shortResName = resourceName.substring( 
resourceName.lastIndexOf( File.separator )+1 );

StringBuffer sb = new StringBuffer();
sb.append( shortName( shortResName, '/' ) )
.append( LINE_SEP )
.append( this.lineNumber )
.append( ' ' )
.append( getLabelWithVariables() ) ;

label = sb.toString();
setLabel( label );

}

return label;
} 

public int getLengthSelection() {
return lengthSelection;
}

public int getLineNumber() {
return lineNumber;
}

public String getResourceName() {
return resourceName;
}

public int getStartPositionSelection() {
return startPositionSelection;
}

public IASTNode getMyNode() {
return myNode;
}

public CodeReviewResource getResource() {
return resource;
}

private String shortName( String sFile, char cSep ) {
int idx = sFile.lastIndexOf( cSep );
return ( idx == -1 ) ? sFile : sFile.substring( idx + 1 );
}
}

这里显示的结果相对来说比较直接。它重载了父类的 getLabel() 方法,以呈现一个通用的结果标签。但是同时,它只包含了基本的特定领域数据。

在类 CodeReviewResult 中,我们有意避免使用 Eclipse 标记,因为在没有文本标记复杂化的基础上,就能充分对其理解。但是,如果您想要创建一个产品级别的 C++ 代码评审 Provider ,那么您就应该使用标记以及注释,以强调某个问题,以及帮助追踪它们的位置。

您将会注意到结果扩展了类 AbstractAnalysisResult,所有的结果都会这样。如果您真想作更多的工作,您可以运行 IAnalysisResult 接口,但是,只是在抽象不能满足您的需要这种少数情况下,您才有必要去这样作。

创建一个基本 Provider 类

因为新的项目和一些前件支持定义的类,所以现在您就可以创建一个分析 Provider 类。

  1. 在插件项目的 analysis.mycpp 包中,创建一个名为 MyCppProvider 的新类。所有的 Provider 必须扩展 AbstractAnalysisProvider 类,该类能够提供一些 Provider 的功能。所有 Provider 类需要的是一个 analyze() 方法。
  2. 编辑您的 Provider 类,代码与列表 4 所示的代码类似。
列表 4. 编辑 Provider 类
package analysis.mycpp;

import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.rsaz.analysis.core.history.AnalysisHistory;
import com.ibm.rsaz.analysis.core.provider.AbstractAnalysisProvider;

public class MyCppProvider extends AbstractAnalysisProvider {
public static final String RESOURCE_PROPERTY = "mycpp.codereview.cpp.resource";
private static final String EXTENSIONS = "c,cpp,h,hpp,hxx";

public void analyze( IProgressMonitor monitor, AnalysisHistory history ) {
}
}

该 Provider 只处理 C 和 C++ 文件,因此,您需要忽略其他种类的文件,因为这些文件可能导致您的剖析器产生错误的结果。幸运的是。静态分析API 在 AnalysisUtil 类中包含了简化的支持。

  1. 添加下面的代码,以作为 Provider analyze() 方法的第一行。
// Obtain the list of valid resource
List<IResource> filteredResources = AnalysisUtil.getFilteredResources(
this.getResources(), EXTENSIONS, CProjectNature.C_NATURE_ID );
// Save the list of resources that were analyzed
history.setAnalyzedResources( filteredResources );

这些代码行从传递给 Provider 的整个列表中,构建了文件源的一个分列表,它只包含了 C 和 C++ 语言使用的扩展文件。为了对所有这些文件进行代码复查,您需要通过该列表进行重复。

  1. 向方法 analyze() 的末尾添加如下的代码:
for( Iterator it = filteredResources.iterator(); it.hasNext(); ) 
{ 
    CodeReviewResource codeReviewRes=new CodeReviewResource((IResource)it.next() ); 
    setProperty( history.getHistoryId(), RESOURCE_PROPERTY, codeReviewRes ); 
    }

该代码通过每个资源进行遍历,并创建了您在前面创建的类 CodeReviewResource (C++ 剖析器)的一个实例。该代码同时还设置了包含该实例的 Provider 的属性。

提示:
扩展 AbstractAnalysisProvider 的分析 Provider 拥有一个属性列表,该列表可被用于共享有关子类别与规则的信息。另外如果有需要,它还可被用于共享规则之间的信息。C/C++ 分析 使用该设置,来向每个规则传递当前的代码评审资源。注意每一个分析历史包含了它自己的属性列表。因此,任意一个 Provider 可能都有一些属性设置,当用户在分析结果视图中删去一条历史记录时,这些属性设置将会被自动删去。这就是该设置的 API。

for( Iterator it = filteredResources.iterator(); it.hasNext(); ) 
{
 CodeReviewResource codeReviewRes=new CodeReviewResource((IResource)it.next() );
 setProperty( history.getHistoryId(), RESOURCE_PROPERTY, codeReviewRes );
}

Provider 还需要注意过程监视器,因为用户需要反馈,并可能选择取消分析过程。

  1. 编辑方法 analyze() 让它和列表 5 相似。
列表 5. 编辑 analyze() 方法
public void analyze( IProgressMonitor monitor, AnalysisHistory history ) {
// Obtain the list of valid resource
List<IResource> filteredResources = AnalysisUtil.getFilteredResources( 
this.getResources(), EXTENSIONS, CProjectNature.C_NATURE_ID );


// Save the list of resources that were analyzed
history.setAnalyzedResources( filteredResources );
monitor.beginTask( getLabel(), filteredResources.size() );

// Iterate through each selected resource file
for( IResource resource : filteredResources ) {
if( monitor.isCanceled() ) {
tearDown();
return;
}

CodeReviewResource codeReviewRes = new CodeReviewResource( resource, true); 
setProperty( history.getHistoryId(), RESOURCE_PROPERTY, codeReviewRes );

// Do work here

monitor.worked( 1 );
}

removeProperty(history.getHistoryId(), RESOURCE_PROPERTY); 
monitor.done();
tearDown();
}

一个 Provider 的 analyze() 方法的基本任务,是向它拥有的所有类别发送一条“analyze”信息。但是,因为用户可能并没选择所有的类别,因此 Provider 需要去确保,只向那些用户选中的类别发送该条信息。解决该问题的方案是,通过 Provider 含有的元素(通常只限于类别元素)进行迭代,并访问它们的 analyze() 方法。

  1. 在 Provider analyze() 方法中,用以下的代码取代“Do work here”注释:
for( AbstractAnalysisElement element : getOwnedElements() ) {
if( monitor.isCanceled() ) {
tearDown();
return;
}

// Get the unbquitous category
DefaultAnalysisCategory category = (DefaultAnalysisCategory)element;
if( history.containsAnalysisElement( category ) ) {
category.analyze( history );
}
}

您需要再次检查过程监视器,以确保用户并没有取消分析操作。为了测试一个类别是否可用,您可以利用历史实例的特征。方法 AnalysisHistory.containsAnalysisElement() 接受任何分析元素,并在静态分析过程开始前,测试它是否被用户选中。如果含有的类别出现在历史中,那么 Provider 就可以访问它的 analyze() 方法。

C/C++ 代码评审的完整分析 Provider 类,应该与列表 6 中的代码类似。唯一增加的是访问类的末尾,以除掉您设置的属性,这只是一项预防措施,确保存储的不会意外的逐渐消失。

列表 6. C/C++ 代码评审的完整分析提供类
package com.ibm.rsaz.analysis.codereview.cpp;

import java.util.List;

import org.eclipse.cdt.core.CProjectNature;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;

import com.ibm.rsaz.analysis.core.AnalysisUtil;
import com.ibm.rsaz.analysis.core.category.DefaultAnalysisCategory;
import com.ibm.rsaz.analysis.core.element.AbstractAnalysisElement;
import com.ibm.rsaz.analysis.core.element.AnalysisParameter;
import com.ibm.rsaz.analysis.core.history.AnalysisHistory;
import com.ibm.rsaz.analysis.core.provider.AbstractAnalysisProvider;
import com.ibm.rsaz.analysis.core.result.AbstractAnalysisResult;
import com.ibm.rsaz.analysis.core.rule.AbstractAnalysisRule;

public class CodeReviewProvider
extends AbstractAnalysisProvider
{
private static final String EXTENSIONS = "c,cpp,h,hpp,hxx";
public static final String RESOURCE_PROPERTY = "codereview.cpp.resource";



/**
* Analyze the categories selected by the user
* 
* @param parentMonitor The progress monitor of the parent task
* @param history A reference to the history record for this analysis
* 
*/
public void analyze( IProgressMonitor monitor, AnalysisHistory history ) {
// Obtain the list of valid resource
List<IResource> filteredResources = AnalysisUtil.getFilteredResources(
this.getResources(), EXTENSIONS, CProjectNature.C_NATURE_ID );


// Save the list of resources that were analyzed
history.setAnalyzedResources( filteredResources );

monitor.beginTask( getLabel(), filteredResources.size() );


// Iterate through each selected resource file
for( IResource resource : filteredResources ) {
if( monitor.isCanceled() ) {
tearDown();
return;
}

CodeReviewResource codeReviewRes = new CodeReviewResource( 
resource, true );
setProperty( history.getHistoryId(), RESOURCE_PROPERTY, codeReviewRes );

for( AbstractAnalysisElement element : getOwnedElements() ) {
if( monitor.isCanceled() ) {
tearDown();
return;
}

// Get the unbquitous category
DefaultAnalysisCategory category = (DefaultAnalysisCategory)element;
if( history.containsAnalysisElement( category ) ) {
category.analyze( history );
}
}

monitor.worked( 1 );
}

removeProperty(history.getHistoryId(), RESOURCE_PROPERTY); 
monitor.done();
tearDown();
}
}

创建一个 Provider 插件扩展

随着现在 Provider 类的完成,您又面临一个新的问题,因为现在您需要将此项新功能插入 Eclipse 中。解决这个问题的方法很简单:您可以定义一个扩展点,从而允许分析工具找到 C++ 分析功能。

  1. 打开插件声明文件,并选择 Extensions 项。
  2. 点击 Add 按钮,并在 New Extension 目录下,选择 com.ibm.rsaz.analysis.core.analysisProvider 扩展点。该扩展点用于识别新的分析 Provider。
  3. 分析 Provider 扩展现在应该列于 All Extensions 列表中。右击该条目,并选择 New > analysisProvider,如图 6 所示。
图 6. 定义扩展点
菜单的图

选择添加的 analysisProvider 条目,完成该表,使其如图 7 所示:

  • 注意 Class 区域包含了在前面章节中创建的 Provider 类的名字。
  • ID 区域应该是一个足够独一无二的标识符(类名可以很适合它,但是它可以是任何独一无二的字符串。
  • Label 区域包含了,当分析配置目录可见时,对用户可见的字符串。理想条件下,标签会是一个从 plugin.properties 文件中获得的固定字符串,但是在目前的情况下,使用一个硬编码是为了简化性。

我们将在稍后讨论剩余的区域。现在,把它们留成空白,如图 7 所示。

图 7. 完成扩展元素细节
工作区的图

信不信由您,您所要做的只是向静态分析框架,插入一个 Provider。如果您为您的 Eclipse 开发环境运行一个实时工作台的话,您可以证明,您的新 Provider 现在就可以使用了(参见图 8)。

图 8. 验证分析 Provider
工作区的图

当然在这个地方, Provider 是不可用的,因为它没有定义任何种类或者规则。但是它定义了一个真实的分析规则引擎。所有现在,让我们给它一个目的。

添加一个分析类别

完成一个实用分析 Provider 的第一步,是添加一个或多个类别。所有的类别必须扩展 DefaultAnalysisCategory 类,对于您将要创建的任何类别,您都可以直接使用该类。但是,您也可以扩展该类并超越它的方法,这样如有需要,它就可以提供额外的功能。因为 C++ 的类别没有任何特别的地方,所以您就可以直接使用默认的类。但是如果您仔细检查该类的话,您就会发现,同一个分析 Provider 类一样,它有一个定义的 analyze() 方法,如列表 7 所示。

列表 7. DefaultAnalysisCategory 类
public void analyze( AnalysisHistory history ) {
// Analyze any nested categories
List categories = getOwnedElements();
if( categories != null ) {
for( Iterator it = categories.iterator(); it.hasNext(); ) {
AbstractAnalysisElement element = (AbstractAnalysisElement)it.next();
if( history.containsAnalysisElement( element ) ) {
element.addHistoryResultSet( history.getHistoryId() );

if( element.getElementType() == CATEGORY_ELEMENT_TYPE ) {
DefaultAnalysisCategory category = (DefaultAnalysisCategory) element;
try {
category.analyze(history);
} catch (Exception e) {
Log.severe(CoreMessages.bind(
CoreMessages.execute_analyzeCategory_failure, category.getLabel()),e);
}
} else if( element.getElementType() == RULE_ELEMENT_TYPE ) {
AbstractAnalysisRule rule = (AbstractAnalysisRule)element;
try {
AnalysisHistoryElement historyElement = history.getHistoryElement(rule);
historyElement.startElapsedTimer();
rule.analyze(history);
historyElement.stopElapsedTime();
} catch (Exception e) {
Log.severe(CoreMessages.bind(
CoreMessages.execute_analyzeRule_failure, rule.getLabel()),e);
}

}
}
}
}
}

该段代码中重要的一点是,显示了一个类别可以拥有规则或者其他类别(它们可以被联系在一起)。由于这个原因,在任何含有元素的 analyze() 方法可以被访问之前,类别需要知道,该元素是一个规则,还是一个类别。它使用 AbstractAnalysisElement.getElementType() 方法来解决这个问题。

这里另一个需要注意的地方,是在执行一项规则之前,代码开始后,在规则完成的时候终止代码。这用于追踪每项规则的总的执行时间。

如果您想要创建您自己的通用类别,并执行一个新 analyze() 方法的话,您需要意识到以上注意点,并确保您自己的类别以同样的方式运行 。否则,您将会遇到无法预料的状况,避免这种情况的最简单的方式是,复制默认的类别 analyze() 方法,并修改它使其满足您的需要。

在您拥有一个类别类以后(在目前的情况下是 DefaultAnalysisCategory),您需要将它与您的 Provider 联系起来。

  1. 为了完成这点,您需要再一次返回到插件声明,并添加一个新的扩展。这次是:com.ibm.rsaz.analysis.core.analysisCategory
  2. 右击该扩展,添加一个新的 analysisCategory,并得到如图 9 所示的结果表。
图 9. 将类别类与分析 Provider 联系起来
工作区的图

注意 Provider 区域的内容是,早期创建的 Provider 的唯一标识符。这意味着,最高层的新类别直接为 Provider 所有。当创建一个亚类别时,Provider 区域应该被留成空白。与之不同的是,表格中的 Category 区域可以输入父类别的 ID。

添加一条规则

规则创建在本系列文章的第 2 部分已详细解释过( C++ Provider 规则结构与讨论的 Java 有细微的差别),所以我们在这里不再赘述。您可能会想,添加一条新规则,要比创建规则类和向插件添加一个扩展容易。

本例中的规则类,如同在同一行中定义多个变量,例如:

int i,j,k;
  1. 在插件的 src 文件夹中,创建一个名为 RuleMutipleDeclarations 的新类。
  2. 向该类添加如列表 8 所示的代码。

列表 8. 添加一条规则

列表 8. 向 RuleMutipleDeclarations 类添加的代码
package analysis.mycpp;

import java.util.Iterator;
import java.util.List;

import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTSimpleDeclaration;

import com.ibm.rsaz.analysis.core.history.AnalysisHistory;
import com.ibm.rsaz.analysis.core.rule.AbstractAnalysisRule;

public class RuleMutipleDeclarations
extends AbstractAnalysisRule
{
/**
* Analyze this rule
* 
* @param history A reference to the history record for this analysis
*/
public void analyze( AnalysisHistory history) {
CodeReviewResource resource = (CodeReviewResource)getProvider().getProperty( 
history.getHistoryId(), MyCppProvider.RESOURCE_PROPERTY );

List list = resource.getTypedNodeList( resource.getResourceCompUnit(),
CodeReviewVisitor.TYPE_IASTDeclaration );
for( Iterator it = list.iterator(); it.hasNext(); ) {

// We only want to work with simple declarations
IASTDeclaration decl = (IASTDeclaration)it.next();
if( decl instanceof IASTSimpleDeclaration ) {
IASTSimpleDeclaration simpleDecl = (IASTSimpleDeclaration)decl;

// If there is more than one declaration, create a match
if( simpleDecl.getDeclarators().length > 1 ) {
resource.generateResultsForASTNode( this,
history.getHistoryId(), decl );
}
}
}
}
}

我们不会深入讨论它的具体工作细节,但是基本来说,它首先参考类 CodeReviewResource ,您可以回忆一下,该类就是被分析的 C++ 类 Provider 创建的剖析器。然后该类使用一些剖析器 API ,以得到声明的一个列表。每个声明将被测试,以确保声明的数目只限于 1 (一个) 。如果不是,通过使用来自您早期创建的 CodeReviewResource 类的附加 API,来产生一个结果。

  1. 过程的最后一步,是通过使用 com.ibm.rsaz.analysis.core.analysisRule 扩展点来添加规则扩展 。确保您的扩展,与图 10 中显示的扩展相似。(注意 category 区域的内容,是在前面段落中创建的类别的唯一标识符)。

在本系列文章的第 2 部分讨论过的其他方面,在这里也得到应用,例如,您可以寻求自动代码设置,用户帮助以及等等之类的支持。

图 10. 添加规则扩展
工作区的图

结果检查

到目前为止,您还不能显示在分析结果视图中得到的结果。这是分析浏览器需要做的, Rational Software Analyzer 框架提供接口 IAnalysisViewer,以及管理该接口的名为 AbstractAnalysisViewer 的一个抽象类。回忆由 C++ 规则创建的结果,包含了大量的有用信息,包括 CDT 节点信息,行号,以及更多。当您想要检查一个结果时,您可以利用这些信息,在文本编辑器中定位 C++ 源文件,并强调出问题文本。

在您可以运行分析检查器之前,您需要对其有一定了解。检查器出于呈递规则结果数据考虑,可以与任何分析元素联系起来。例如,检查器不需要与一个规则相联系, Provider 与类别也可以参考检查器。在这种情况下,检查器可适用于, Provider 或者类别的所有权链内的任何规则。考虑如下规则结构:

Provider A
Category B
Rule C
Rule D
Category E
Rule F

假设检查器与 Provider A 向联系,当 getViewer() 方法访问规则 C,D 或者 F 时,它们将会首先检查它们自己的检查器参考。如果没有发现任何检查器,那么规则就会参考它们拥有的类别。在这种情况下,类别并没有分配的检查器,因此,它们将参考它们拥有的 Provider ,该 Provider 返回您分配的检查器。现在进一步假设,向 Rule C 分配了一个不同的检查器。那么在这种情况下,除了 C 以外的所有规则,将使用 Provider 的同一默认检查器,而 C 将会使用分配给它的检查器。

退回方案允许单个检查器,去处理所有呈递的规则结果,并允许不同的检查器联系树状图的任何地方 ,以替换默认的行为。在理解这个之后,让我们去浏览一个检查器实例 。

列表 9. 检查器代码实例
package analysis.mycpp;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.ITextEditor;

import com.ibm.rsaz.analysis.core.AnalysisConstants;
import com.ibm.rsaz.analysis.core.logging.Log;
import com.ibm.rsaz.analysis.core.result.AbstractAnalysisResult;
import com.ibm.rsaz.analysis.core.viewer.IAnalysisViewer;

public class CodeReviewViewer implements IAnalysisViewer {
public void showView( final AbstractAnalysisResult result )
{

final CodeReviewResult specificResult = (CodeReviewResult)result;

Job job = new Job( "Open C++ File" ) {
protected IStatus run( IProgressMonitor monitor )
{
Display.getDefault().asyncExec(new Runnable() {
public void run() {
Path path = new Path(specificResult.getResourceName() ); 
IFile file = ResourcesPlugin.getWorkspace()
.getRoot().getFile(path);

if ( ( file != null ) && file.exists() ) {
try {
ITextEditor editor = (ITextEditor)IDE.openEditor(
PlatformUI.getWorkbench()
.getActiveWorkbenchWindow()
.getActivePage(), file, true ); 

// If there are unsaved changes in the editor,
// save them before trying to highlight a result
if( editor.isDirty() ) {
editor.doSave( null );
}

editor.selectAndReveal( specificResult
.getStartPositionSelection(),
specificResult.getLengthSelection() );

} catch ( PartInitException e ) {
Log.severe( AnalysisConstants.BLANK, e );
}
}
}
});
return Status.OK_STATUS;
}
};
job.schedule();
}
}

在本例中有几个需要注意的地方:

  • 检查器类执行 IAnalysisViewer 接口,并使该接口调用 showView() 方法。 简单来说,检查器代码从结果中获取资源,并为该资源创建一条路径。该路径被传递给文本编辑器用于显示,最终,编辑器被定位,这样 CDT 节点在编辑器中就可见并被选中。
  • 为了改进用户的感受,所有的工作都应该在 Eclipse 中完成。

为了让该检查器类具有功能,您需要向插件文件添加另外一个扩展:

  1. 打开 plugin.xml 文件,并选择 Extensions 项。
  2. com.ibm.rsaz.analysis.core.analysisViewer 添加一个新的扩展,并添加该表格的内容,如图 11 所示。
图 11. 激活检查器类
工作区的图
  1. 接下来,通过在 Provider 提供的区域,输入检查器的唯一标识符,来将检查器与 Provider 联系起来(参见图 12)。
图 12. 将检查器与 Provider 联系起来
工作区的图

最后一步,您需要纠正检查器类中的编辑错误。这意味着,在 Eclipse IDE 中需要应用附加的插件附件,以支持编辑。

  1. 选择 Dependencies 项,并向所需插件的列表添加 org.eclipse.ui.workbench.texteditor org.eclipse.ui.ide
  2. 为了测试检查器,再次运行 C++ 分析工具(对产生结果的源代码)。
  3. 在分析结果视图中双击一个结果。Eclipse 工作台将打开一个编辑器,该编辑器包含并显示出了与正确类对比的错误。

外部静态分析引擎

分析 Provider 被忽略的一个方面,是它可以集成已存在分析引擎的能力。既然该 Provider 与特定领域的引擎密切相关,我们在这里并不讨论它。但是,假设有 API 访问了引擎,那么向 Rational Software Analyzer 框架插入该 API 就成为可能。在这种情况下, Provider ,类别以及规则可以简单集中了,外部引擎具有的功能,例如:

  • 如果外部引擎已经支持类别,优先考虑 Provider 中的 $getOwnedElements() 方法,以返回在类 DefaultAnalysisCategory 中的类别列表。
  • 如果规则存在,优先考虑类别类中的相同 getOwnedElements() 方法,以返回一列 AbstractAnalysisRule 实例,该实例反映了真实的分析引擎支持什么。

总结以及后续

在本篇文章中,我们已经学习了 APIs,以及从底层构建新分析 Provider 需要的大多数方面 ,包括类别以及规则的定义。同时我们还简略的学习了,怎样去定义一个分析结果类,并给出了剖析器的概述。基本上,不管用于分析的语言是哪种,构建 Provider 的步骤与例子中的大体相似。

在本系列的后续文章中,我们将寻求访问 Rational Software Analyzer 功能的方法,以帮助向任何的构建系统集成该工具。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational
ArticleID=350411
ArticleTitle=使用 IBM Rational Software Analyzer 进行静态分析,第 4 部分: 集成您自己的分析工具
publish-date=11072008