高级PRQL功能
高级PRQL功能
本节详细介绍 PRQL(Code Query LINQ)的高级功能,这是 CppDepend 用于分析 C/C++ 代码的查询语言。从查询债务和质量门禁到探索代码依赖关系和命名约定,PRQL 为深度代码分析提供了强大的功能。
查询债务、问题、规则和质量门
简介
自 CppDepend v2017.1 推出以来,PRQL 不仅用于代码查询,还用于查询债务、问题、规则和 质量门。
此功能有助于深入探索技术债务。在 技术债务文档 我们演示了如何通过仪表板上的几次点击生成查询来探索债务和问题。
此功能也可用于定义 自定义趋势度量, Quality Gates 和 规则.
例如,一个定义技术债务百分比阈值的质量门可能如下所示:
1// <QualityGate Name="Percentage Debt" Unit="%" />23failif value > 30%45warnif value > 20%67let timeToDev = codeBase.EffortToDevelop()89let debt = Issues.Sum(i => i.Debt)1011select 100d * debt.ToManDay() / timeToDev.ToManDay()
一个统计被违反关键规则数量的趋势指标可能如下所示:
1// <TrendMetric Name="# Critical Rules Violated" Unit="rules"/>23from rule in Rules45where rule.IsViolated() && rule.IsCritical67select new {89 rule,1011 issues = rule.Issues(),1213 debt = rule.Debt(),1415 annualInterest = rule.AnnualInterest(),1617 maxSeverity = rule.Issues().Max(i => i.Severity)1819}
此趋势指标不仅有助于跟踪趋势,其结果还可浏览以进行深入探索:

查询自基线以来的差异
当基线可用时,除了对当前代码库快照运行规则外,还会对基线运行规则。因此,CppDepend 可以比较两个问题集:在基线上运行规则获得的问题集和在当前代码库快照上运行规则获得的问题集。
然后可以使用 PRQL 查询债务、问题、规则和质量门禁的差异。例如,一个统计自基线以来新问题的趋势指标可能如下所示:
1// <TrendMetric Name="# New Issues since Baseline" Unit="issues"/>23from issue in Issues45where issue.WasAdded()67select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }
一个禁止自基线以来超过 2 人日技术债务的质量门可能如下所示:
1// <QualityGate Name="New Debt since Baseline" Unit="man-days" />23failif value > 2 man-days45warnif value > 0 man-days67let debt = Issues.Sum(i => i.Debt)89let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt)1011select (debt - debtInBaseline).ToManDay()
默认定义了一打质量门禁,并且很容易自定义和创建新的。在下面的屏幕截图中,这个查询(通过在仪表板上单击一次生成)不仅显示了质量门禁的当前状态,还显示了基线上的质量门禁状态。依赖差异的质量门禁无法针对基线运行,这就是为什么它们有一个 不可用 (N/A) 值。

同样,许多与债务、问题、规则和质量门禁相关的趋势指标默认已定义,并且可以轻松自定义和创建新指标。

仪表板提供多个菜单,用于生成查询以探索债务、问题、规则和质量门禁的状态。任何数字也都是可点击的,可以生成列出所计项目的查询。

工作原理
CppDepend.API 定义了专门的类型来指定债务模型,包括 债务 ; IIssue ; IRule ; IQualityGate ; QualityGateStatus.
然而两种关键类型是: IIssuesSet ; IIssuesSetDiff.
- 首先,CppDepend 在当前快照和基线上都运行已激活的规则。
- 它计算问题、债务数值和差异。
- 然后填充这些 issues-set 和 issues-set-diff 对象。
- 依赖 issues-set 和 issues-set-diff 的查询只有在这些集合被填充后才会执行。因此,规则不能依赖这些集合,但质量门禁可以。
而不是编写像这样的查询...
1from i in context.IssuesSet.AllIssues select i
...或像...
1from i in context.IssuesSetDiff.OlderIssuesSet.AllIssues select i
...PRQL提供4个域: 问题, IssuesOnBaseline, 规则 和 QualityGates.
这些域是的快捷方式 context.IssuesSet.AllIssues, context.IssuesSetDiff.OlderIssuesSet.AllIssues, context.IssuesSet.AllRules 和 context.IssuesSet.AllQualityGates.
这些域可视为以下类型的范围变量: IEnumerable
有了这些域,就可以编写简单的查询,例如……
1from i in Issues select i
...甚至只是:
问题
同样,不必不断引用 issues-set 和 issues-set-diff 来获取数据,例如……
1from codeElement in CodeElements23where context.IssuesSet.HasIssue(codeElement)45select new {67 codeElement,89 issues = context.IssuesSet.Issues(codeElement),1011 newIssues = context.IssuesSet.Issues(codeElement)1213 .Where(i => context.IssuesSetDiff.WasAdded(i))1415}
...类型 IssuesSet 和 IssuesSetDiff 提供便捷的扩展方法,这些方法由 PRQL 编译器自动转换为对 context.IssuesSet 和 context.IssuesSetDiff 的调用。
例如上面的查询可以重写为:
1from codeElement in CodeElements23where codeElement.HasIssue()45select new {67 codeElement,89 issues = codeElement.Issues(),1011 newIssues = codeElement.NewerIssues()1213}
提议的扩展方法完整列表如下:
- HasIssue() / HasNewIssue() / HasIssueOnBaseline()
- Debt() / NewDebt() / DebtOnBaseline()
- AnnualInterest() / NewAnnualInterest() / AnnualInterestOnBaseline()
- Issues() / NewerIssues() / IssuesOnBaseline()
- RulesViolated() / RulesViolatedOnBaseline()
- QualityGatesStatus() / QualityGatesStatusOnBaseline()
请注意,使用像这样的属性时 AnnualInterest,类型 债务 返回的定义到隐式转换 TimeSpan (因为债务是修复所需的一定人工时间)。因此子查询可以写成:
1let annualInterest = Issues.Sum(i => i.AnnualInterest)
这里 annualInterest 可以是类型 债务 或 TimeSpan 对于如下子表达式也是如此:
1let debt = codeBase.TechnicalDebt23let annualInterest = debt.AnnualInterest
...可以重写为:
1let annualInterest = codeBase.TechnicalDebt.AnnualInterest
查询代码对象模型
每次编写代码查询时,您都是在针对代码库模型的类型、方法和字段集编写查询。因此,代码元素是核心对象。
下面是一个简单的查询,展示如何枚举一个类的基类:
1from t in Application.Types where t.NameLike ("MyClass")23select new {45 t,67 baseClasses = t.BaseClasses }
下面是一个查询,展示如何枚举被某方法重写的方法,以及重写某方法的方法:
1from m in Application.Methods where m.NameLike ("MyMethod")23select new {45 m,67 // Enumerates methods overridden by m89 m.OverriddensBase,1011 // Enumerates methods that overrides m directly1213 m.OverridesDirectDerived,1415 // Enumerates methods that overrides m directly or indirectly1617 m.OverridesDerived }
查询代码依赖和设计
依赖模型是 常规 因为它不依赖于使用类型(字段赋值、方法调用……)。 一个 和 B 对于两个代码元素,我们说 一个 依赖于 B 如果、当 B 不可用, 一个 无法编译。
以下是依赖查询示例:
1from m in Methods where23 m.IsUsing("MyType") &&45 m.IsUsedBy("MyProjectName".AllowNoMatch())67select m
请注意此类扩展方法如何被从以下生成的规则所使用 依赖关系图 或 依赖矩阵,以禁止某个特定依赖:

生成的规则如下所示。它可以轻松调整以禁止或强制代码库中的任何依赖关系。

CppDepend 还提供了一种查询代码库依赖关系的便捷方式(并且通常 更快 同样):
1from t in Types.UsedByAny(Types.Where(t => t.IsStatic)) select t
间接使用
CppDepend 提供了一些包含该词的方法 间接 在名称中,以处理间接依赖关系,例如该方法 IsIndirectlyUsing().
例如如果 一个 正在使用 B 正在使用 C, 一个 不是 直接 using C 但是 一个 是 间接地 using C (深度为 2).
下面是一个枚举直接或间接调用 Print 方法的方法的查询 MyClass.Print().
1from m in Methods23 let depth0 = m.IsIndirectlyUsing("MyClass.Print()")45select m
DepthOfIsUsing 方法更进一步,因为它返回使用深度。
1from m in Methods23 let depth0 = m.DepthOfIsUsing("MyClass.Print()")45 where depth0 >= 0 orderby depth0 ascending67select new { m, depth0 }
返回的深度是 可空
- 这是 null 如果 m 不间接调用目标方法。
- 这是 1 如果 m 直接调用目标方法。
- 它大于 1 如果 m 间接调用目标方法。
这种间接使用方式对于生成以下内容特别有用 调用图 或 类继承图.
一些方法还包含单词 间接 或单词 深度 以处理从序列到代码元素的间接使用,甚至 任何 目标序列的代码元素 (后缀 任意).
例如,以下查询匹配所有正在调用的方法, 直接 或 间接地 静态方法,使用 深度 调用的:
1let statics = Methods.Where(m => m.IsStatic).ToHashSet()23 let depthMetric = Application.Methods.DepthOfIsUsingAny(statics)45 from m in depthMetric.DefinitionDomain67 let depthValue = depthMetric[m]89 orderby depthValue ascending1011select new { m, depthValue }
查询代码质量和代码度量
CppDepend计算超过 80个代码度量.
1// <Name>Quick summary of methods to refactor</Name>23warnif count > 0 from m in JustMyCode.Methods where45 // Code Metrics' definitions67 m.NbLinesOfCode > 30 || // http://www.cppdepend.com/documentation/code-metrics#NbLinesOfCode89 m.CyclomaticComplexity > 20 || // https://www.cppdepend.com/documentation/code-metrics#CC1011 m.NestingDepth > 5 || // http://www.cppdepend.com/documentation/code-metrics#ILNestingDepth1213 m.NbParameters > 5 || // http://www.cppdepend.com/documentation/code-metrics#NbParameters1415 m.NbVariables > 8 || // http://www.cppdepend.com/documentation/code-metrics#NbVariables1617 m.NbOverloads > 6 // http://www.cppdepend.com/documentation/code-metrics#NbOverloads1819select new { m, m.NbLinesOfCode, m.CyclomaticComplexity,2021 m.NbParameters, m.NbVariables, m.NbOverloads }
请注意,许多代码度量返回可空数值,因为它们不一定对所有代码元素都有定义。例如 #代码行数 不为第三方代码元素计算。
自定义度量
借助 LINQ 的灵活性,可以轻松组合默认代码指标以创建更精细的代码指标,例如:
1// <Name>Custom metric</Name>23warnif count > 045from m in JustMyCode.Methods67// Don't match too short methods89where m.NbLinesOfCode > 101011let CC = m.CyclomaticComplexity1213let CustomMetric = (CC * CC )/1001415where Custom != null && Custom > 301617orderby Custom descending, m.NbLinesOfCode descending1819select new { m, Custom, CC, m.NbLinesOfCode }
自定义代码指标对象也可以通过该方法获得 FillIterative()。例如,此功能在规则中用于获取死类型,也用于获取仅被死类型使用的类型(以及使用深度):
1// <Name>Potentially dead Types</Name>23warnif count > 045// Filter procedure for types that should'nt be considered as dead67let canTypeBeConsideredAsDeadProc = new Func<IType, bool>(89 t => !t.IsPublic && // Public types might be used by client applications of your projects.1011 !t.IsGeneratedByCompiler)1213// Select types unused1415let typesUnused =1617 from t in JustMyCode.Types where1819 t.NbTypesUsingMe == 0 && canTypeBeConsideredAsDeadProc(t)2021 select t2223// Dead types = types used only by unused types (recursive)2425let deadTypesMetric = typesUnused.FillIterative(2627 types => from t in codeBase.Application.Types.UsedByAny(types).Except(types)2829 where canTypeBeConsideredAsDeadProc(t) &&3031 t.TypesUsingMe.Intersect(types).Count() == t.NbTypesUsingMe3233 select t)3435from t in deadTypesMetric.DefinitionDomain3637select new { t, t.TypesUsingMe, depth = deadTypesMetric[t] }
查询代码差异
CppDepend具有独特的功能 比较代码库的两个不同快照 以查看更改/添加/删除了什么。
例如,以下查询枚举代码已更改的方法:
1from m in Application.Methods23where context.CompareContext.CodeWasChanged(m)45select m
实际上,您只需编写下面最短的查询,PRQL 编译器会负责将其转换为上面的查询:
1from m in Application.Methods23where m.CodeWasChanged()45select m
编写这样的查询后,CppDepend UI 提供比较已更改方法的两个源文件版本的功能。

注意 OlderVersion() 和 NewerVersion() 这两个方法。顾名思义,这些方法返回代码元素的较旧或较新版本,或者 null 代码元素是新增的(因此没有旧版本)还是已删除的(因此没有新版本)。例如,这些方法可用于编写下面跟踪方法复杂度演变的查询:
1// <Name>Methods that became more complex</Name>23from m in codeBase.OlderVersion().Methods45where m.IsPresentInBothBuilds()67let oldCC = m.CyclomaticComplexity89let newCC = m.NewerVersion().CyclomaticComplexity1011where oldCC != null && newCC > oldCC1213select new { m, oldCC, newCC }
注意对的调用 IsPresentInBothBuilds 以确保我们只处理同时存在于旧代码库快照和新代码库快照中的方法,从而防止任何 NullReferenceException 运行查询时!
如最后这个查询所示,混合 差异 功能与其他 PRQL 功能(如代码质量度量或依赖关系)相结合,可以产生强大的代码查询和规则,用于跟踪代码库的演变。
查询代码元素的命名
PRQL 提供了多种查询代码元素名称的功能。这对于编写以下内容特别有用 简单 代码命名约定 (基于 正则表达式)...
1// <Name>Abstract base class should be suffixed with 'Base'</Name>23warnif count > 0 from t in Application.Types where45 t.IsAbstract &&67 t.IsClass &&89 t.DepthOfInheritance == 1 &&1011 ((!t.IsGeneric && !t.NameLike (@"Base$")) ||1213 ( t.IsGeneric && !t.NameLike (@"Base<")))1415select new { t, t.DepthOfInheritance }
...或 智能 代码命名约定:
1// <Name>Avoid naming types and namespaces with the same identifier</Name>23// Not only this can provoke compiler resolution collision,45// but also, this makes code less maintainable because67// concepts are not concisely identified.89warnif count > 01011let hashsetShortNames = Namespaces.Where(n => n.Name.Length > 0).Select(n => n.SimpleName).ToHashSet()1213from t in JustMyCode.Types1415where hashsetShortNames.Contains(t.Name)1617select new { t, namespaces = Namespaces.Where(n => n.SimpleName == t.Name) }
代码元素命名功能在 PRQL 语法文档中有概述,见以下章节 按名称字符串匹配代码元素.
查询状态可变性
不可变性的概念越来越流行。在多线程环境中处理并发访问时,不可变性特别有用。
类型被视为 不可变 如果其实例字段在构造函数创建实例后不能被修改。一个 静态 字段被视为 不可变 如果它是私有的,并且仅由静态构造函数赋值。一个 实例 字段被视为 不可变 如果它是私有的,并且仅由其类型的构造函数或其类型的静态构造函数赋值。请注意,声明为以下的字段 只读 必然是不可变的,但一个字段可以不被声明为以下也是不可变的 只读。在最后一种情况下,关键字 只读 可以添加到字段声明中,而不会引起任何编译错误。
在分析时,CppDepend 计算类型和字段的可变性。结果可通过 Type.IsImmutable 和 Field.IsImmutable 属性获取。
这样就可以轻松编写此类规则,例如:
1// <Name>Structures should be immutable</Name>23warnif count > 0 from t in Application.Types where45 t.IsStructure &&67 !t.IsImmutable89let mutableFields = t.Fields.Where(f => !f.IsImmutable)1011select new { t, t.NbLinesOfCode, mutableFields }
ChangesObjectState 和 ChangesTypeState 这两个属性可用于强制或检查某个方法是否 纯。一 纯 方法是不分配任何实例或静态字段的方法。
此外,要控制对特定字段的写访问,您可以使用扩展方法 AssignField():
1from m in Methods where m.AssignField("MyClass.myfield")23select new { m, m.NbLinesOfCode }
此外,CppDepend 为字段提供了 3 个属性 MethodsAssigningMe、MethodsReadingMeButNotAssigningMe 和 MethodsUsingMe,并为方法提供了 AssignField() 方法。
查询源文件路径
对于每个代码元素,您可以访问其 SourceDecls。访问源文件声明开启了一系列有趣的应用。例如,匹配要从……中丢弃的方法的默认查询 JustMyCode 代码库视图,依赖于源文件名的一些模式,并且可以轻松适应任何情况:
1// <Name>Discard generated and designer Methods from JustMyCode</Name>23// --- Make sure to make this query richer to discard generated methods from CppDepend rules results ---45notmycode67//89// First define source files paths to discard1011//1213from a in Application.Projects141516let projectSourceFilesPaths = a.SourceDecls.Select(s => s.SourceFile.FilePath)1718let sourceFilesPathsToDiscard = (1920 from filePath in projectSourceFilesPaths2122 let filePathLower= filePath.ToString().ToLower()2324 where2526 filePathLower.Contains("generated")2728 select filePath2930).ToHashSet()3132//3334// Second: discard methods in sourceFilesPathsToDiscard3536//3738from m in a.ChildMethods3940where (sourceFilesPathsToDiscard.Contains(m.SourceDecls.First().SourceFile.FilePath))4142select new { m, m.NbLinesOfCode }
创建新的 CppDepend 项目时,会提供多条关于源文件组织的默认规则。
