高级PRQL功能

高级PRQL功能

本节详细介绍 PRQL(Code Query LINQ)的高级功能,这是 CppDepend 用于分析 C/C++ 代码的查询语言。从查询债务和质量门禁到探索代码依赖关系和命名约定,PRQL 为深度代码分析提供了强大的功能。

查询债务、问题、规则和质量门

简介

自 CppDepend v2017.1 推出以来,PRQL 不仅用于代码查询,还用于查询债务、问题、规则和 质量门。

此功能有助于深入探索技术债务。在 技术债务文档 我们演示了如何通过仪表板上的几次点击生成查询来探索债务和问题。

此功能也可用于定义 自定义趋势度量, Quality Gates规则.

例如,一个定义技术债务百分比阈值的质量门可能如下所示:

1// <QualityGate Name="Percentage Debt" Unit="%" />
2
3failif value > 30%
4
5warnif value > 20%
6
7let timeToDev = codeBase.EffortToDevelop()
8
9let debt = Issues.Sum(i => i.Debt)
10
11select 100d * debt.ToManDay() / timeToDev.ToManDay()

一个统计被违反关键规则数量的趋势指标可能如下所示:

1// <TrendMetric Name="# Critical Rules Violated" Unit="rules"/>
2
3from rule in Rules
4
5where rule.IsViolated() && rule.IsCritical
6
7select new {
8
9 rule,
10
11 issues = rule.Issues(),
12
13 debt = rule.Debt(),
14
15 annualInterest = rule.AnnualInterest(),
16
17 maxSeverity = rule.Issues().Max(i => i.Severity)
18
19}

此趋势指标不仅有助于跟踪趋势,其结果还可浏览以进行深入探索:

CppDepend中的趋势度量

查询自基线以来的差异

当基线可用时,除了对当前代码库快照运行规则外,还会对基线运行规则。因此,CppDepend 可以比较两个问题集:在基线上运行规则获得的问题集和在当前代码库快照上运行规则获得的问题集。

然后可以使用 PRQL 查询债务、问题、规则和质量门禁的差异。例如,一个统计自基线以来新问题的趋势指标可能如下所示:

1// <TrendMetric Name="# New Issues since Baseline" Unit="issues"/>
2
3from issue in Issues
4
5where issue.WasAdded()
6
7select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }

一个禁止自基线以来超过 2 人日技术债务的质量门可能如下所示:

1// <QualityGate Name="New Debt since Baseline" Unit="man-days" />
2
3failif value > 2 man-days
4
5warnif value > 0 man-days
6
7let debt = Issues.Sum(i => i.Debt)
8
9let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt)
10
11select (debt - debtInBaseline).ToManDay()

默认定义了一打质量门禁,并且很容易自定义和创建新的。在下面的屏幕截图中,这个查询(通过在仪表板上单击一次生成)不仅显示了质量门禁的当前状态,还显示了基线上的质量门禁状态。依赖差异的质量门禁无法针对基线运行,这就是为什么它们有一个 不可用 (N/A) 值。

CppDepend质量门

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

CppDepend趋势监控

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

CppDepend仪表板

工作原理

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.AllRulescontext.IssuesSet.AllQualityGates.

这些域可视为以下类型的范围变量: IEnumerable ; IEnumerable ; IEnumerable.

有了这些域,就可以编写简单的查询,例如……

1from i in Issues select i

...甚至只是:

问题

同样,不必不断引用 issues-set 和 issues-set-diff 来获取数据,例如……

1from codeElement in CodeElements
2
3where context.IssuesSet.HasIssue(codeElement)
4
5select new {
6
7 codeElement,
8
9 issues = context.IssuesSet.Issues(codeElement),
10
11 newIssues = context.IssuesSet.Issues(codeElement)
12
13 .Where(i => context.IssuesSetDiff.WasAdded(i))
14
15}

...类型 IssuesSetIssuesSetDiff 提供便捷的扩展方法,这些方法由 PRQL 编译器自动转换为对 context.IssuesSet 和 context.IssuesSetDiff 的调用。

例如上面的查询可以重写为:

1from codeElement in CodeElements
2
3where codeElement.HasIssue()
4
5select new {
6
7 codeElement,
8
9 issues = codeElement.Issues(),
10
11 newIssues = codeElement.NewerIssues()
12
13}

提议的扩展方法完整列表如下:

  • 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.TechnicalDebt
2
3let annualInterest = debt.AnnualInterest

...可以重写为:

1let annualInterest = codeBase.TechnicalDebt.AnnualInterest

查询代码对象模型

每次编写代码查询时,您都是在针对代码库模型的类型、方法和字段集编写查询。因此,代码元素是核心对象。

下面是一个简单的查询,展示如何枚举一个类的基类:

1from t in Application.Types where t.NameLike ("MyClass")
2
3select new {
4
5 t,
6
7 baseClasses = t.BaseClasses }

下面是一个查询,展示如何枚举被某方法重写的方法,以及重写某方法的方法:

1from m in Application.Methods where m.NameLike ("MyMethod")
2
3select new {
4
5 m,
6
7 // Enumerates methods overridden by m
8
9 m.OverriddensBase,
10
11 // Enumerates methods that overrides m directly
12
13 m.OverridesDirectDerived,
14
15 // Enumerates methods that overrides m directly or indirectly
16
17 m.OverridesDerived }

回到顶部

查询代码依赖和设计

依赖模型是 常规 因为它不依赖于使用类型(字段赋值、方法调用……)。 一个B 对于两个代码元素,我们说 一个 依赖于 B 如果、当 B 不可用, 一个 无法编译。

以下是依赖查询示例:

1from m in Methods where
2
3 m.IsUsing("MyType") &&
4
5 m.IsUsedBy("MyProjectName".AllowNoMatch())
6
7select m

请注意此类扩展方法如何被从以下生成的规则所使用 依赖关系图依赖矩阵,以禁止某个特定依赖:

在CppDepend中禁止依赖

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

CppDepend中的PRQL

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 Methods
2
3 let depth0 = m.IsIndirectlyUsing("MyClass.Print()")
4
5select m

DepthOfIsUsing 方法更进一步,因为它返回使用深度。

1from m in Methods
2
3 let depth0 = m.DepthOfIsUsing("MyClass.Print()")
4
5 where depth0 >= 0 orderby depth0 ascending
6
7select new { m, depth0 }

返回的深度是 可空 值。

  • 这是 null 如果 m 不间接调用目标方法。
  • 这是 1 如果 m 直接调用目标方法。
  • 它大于 1 如果 m 间接调用目标方法。

这种间接使用方式对于生成以下内容特别有用 调用图类继承图.

一些方法还包含单词 间接 或单词 深度 以处理从序列到代码元素的间接使用,甚至 任何 目标序列的代码元素 (后缀 任意).

例如,以下查询匹配所有正在调用的方法, 直接间接地 静态方法,使用 深度 调用的:

1let statics = Methods.Where(m => m.IsStatic).ToHashSet()
2
3 let depthMetric = Application.Methods.DepthOfIsUsingAny(statics)
4
5 from m in depthMetric.DefinitionDomain
6
7 let depthValue = depthMetric[m]
8
9 orderby depthValue ascending
10
11select new { m, depthValue }

回到顶部

查询代码质量和代码度量

CppDepend计算超过 80个代码度量.

1// <Name>Quick summary of methods to refactor</Name>
2
3warnif count > 0 from m in JustMyCode.Methods where
4
5 // Code Metrics' definitions
6
7 m.NbLinesOfCode > 30 || // http://www.cppdepend.com/documentation/code-metrics#NbLinesOfCode
8
9 m.CyclomaticComplexity > 20 || // https://www.cppdepend.com/documentation/code-metrics#CC
10
11 m.NestingDepth > 5 || // http://www.cppdepend.com/documentation/code-metrics#ILNestingDepth
12
13 m.NbParameters > 5 || // http://www.cppdepend.com/documentation/code-metrics#NbParameters
14
15 m.NbVariables > 8 || // http://www.cppdepend.com/documentation/code-metrics#NbVariables
16
17 m.NbOverloads > 6 // http://www.cppdepend.com/documentation/code-metrics#NbOverloads
18
19select new { m, m.NbLinesOfCode, m.CyclomaticComplexity,
20
21 m.NbParameters, m.NbVariables, m.NbOverloads }

请注意,许多代码度量返回可空数值,因为它们不一定对所有代码元素都有定义。例如 #代码行数 不为第三方代码元素计算。

自定义度量

借助 LINQ 的灵活性,可以轻松组合默认代码指标以创建更精细的代码指标,例如:

1// <Name>Custom metric</Name>
2
3warnif count > 0
4
5from m in JustMyCode.Methods
6
7// Don't match too short methods
8
9where m.NbLinesOfCode > 10
10
11let CC = m.CyclomaticComplexity
12
13let CustomMetric = (CC * CC )/100
14
15where Custom != null && Custom > 30
16
17orderby Custom descending, m.NbLinesOfCode descending
18
19select new { m, Custom, CC, m.NbLinesOfCode }

自定义代码指标对象也可以通过该方法获得 FillIterative()。例如,此功能在规则中用于获取死类型,也用于获取仅被死类型使用的类型(以及使用深度):

1// <Name>Potentially dead Types</Name>
2
3warnif count > 0
4
5// Filter procedure for types that should'nt be considered as dead
6
7let canTypeBeConsideredAsDeadProc = new Func<IType, bool>(
8
9 t => !t.IsPublic && // Public types might be used by client applications of your projects.
10
11 !t.IsGeneratedByCompiler)
12
13// Select types unused
14
15let typesUnused =
16
17 from t in JustMyCode.Types where
18
19 t.NbTypesUsingMe == 0 && canTypeBeConsideredAsDeadProc(t)
20
21 select t
22
23// Dead types = types used only by unused types (recursive)
24
25let deadTypesMetric = typesUnused.FillIterative(
26
27 types => from t in codeBase.Application.Types.UsedByAny(types).Except(types)
28
29 where canTypeBeConsideredAsDeadProc(t) &&
30
31 t.TypesUsingMe.Intersect(types).Count() == t.NbTypesUsingMe
32
33 select t)
34
35from t in deadTypesMetric.DefinitionDomain
36
37select new { t, t.TypesUsingMe, depth = deadTypesMetric[t] }

回到顶部

查询代码差异

CppDepend具有独特的功能 比较代码库的两个不同快照 以查看更改/添加/删除了什么。

例如,以下查询枚举代码已更改的方法:

1from m in Application.Methods
2
3where context.CompareContext.CodeWasChanged(m)
4
5select m

实际上,您只需编写下面最短的查询,PRQL 编译器会负责将其转换为上面的查询:

1from m in Application.Methods
2
3where m.CodeWasChanged()
4
5select m

编写这样的查询后,CppDepend UI 提供比较已更改方法的两个源文件版本的功能。

CppDepend中的方法差异

注意 OlderVersion() 和 NewerVersion() 这两个方法。顾名思义,这些方法返回代码元素的较旧或较新版本,或者 null 代码元素是新增的(因此没有旧版本)还是已删除的(因此没有新版本)。例如,这些方法可用于编写下面跟踪方法复杂度演变的查询:

1// <Name>Methods that became more complex</Name>
2
3from m in codeBase.OlderVersion().Methods
4
5where m.IsPresentInBothBuilds()
6
7let oldCC = m.CyclomaticComplexity
8
9let newCC = m.NewerVersion().CyclomaticComplexity
10
11where oldCC != null && newCC > oldCC
12
13select new { m, oldCC, newCC }

注意对的调用 IsPresentInBothBuilds 以确保我们只处理同时存在于旧代码库快照和新代码库快照中的方法,从而防止任何 NullReferenceException 运行查询时!

如最后这个查询所示,混合 差异 功能与其他 PRQL 功能(如代码质量度量或依赖关系)相结合,可以产生强大的代码查询和规则,用于跟踪代码库的演变。

回到顶部

查询代码元素的命名

PRQL 提供了多种查询代码元素名称的功能。这对于编写以下内容特别有用 简单 代码命名约定 (基于 正则表达式)...

1// <Name>Abstract base class should be suffixed with 'Base'</Name>
2
3warnif count > 0 from t in Application.Types where
4
5 t.IsAbstract &&
6
7 t.IsClass &&
8
9 t.DepthOfInheritance == 1 &&
10
11 ((!t.IsGeneric && !t.NameLike (@&quot;Base$&quot;)) ||
12
13 ( t.IsGeneric && !t.NameLike (@&quot;Base&lt;&quot;)))
14
15select new { t, t.DepthOfInheritance }

...或 智能 代码命名约定:

1// <Name>Avoid naming types and namespaces with the same identifier</Name>
2
3// Not only this can provoke compiler resolution collision,
4
5// but also, this makes code less maintainable because
6
7// concepts are not concisely identified.
8
9warnif count > 0
10
11let hashsetShortNames = Namespaces.Where(n => n.Name.Length > 0).Select(n => n.SimpleName).ToHashSet()
12
13from t in JustMyCode.Types
14
15where hashsetShortNames.Contains(t.Name)
16
17select new { t, namespaces = Namespaces.Where(n => n.SimpleName == t.Name) }

代码元素命名功能在 PRQL 语法文档中有概述,见以下章节 按名称字符串匹配代码元素.

回到顶部

查询状态可变性

不可变性的概念越来越流行。在多线程环境中处理并发访问时,不可变性特别有用。

类型被视为 不可变 如果其实例字段在构造函数创建实例后不能被修改。一个 静态 字段被视为 不可变 如果它是私有的,并且仅由静态构造函数赋值。一个 实例 字段被视为 不可变 如果它是私有的,并且仅由其类型的构造函数或其类型的静态构造函数赋值。请注意,声明为以下的字段 只读 必然是不可变的,但一个字段可以不被声明为以下也是不可变的 只读。在最后一种情况下,关键字 只读 可以添加到字段声明中,而不会引起任何编译错误。

在分析时,CppDepend 计算类型和字段的可变性。结果可通过 Type.IsImmutable 和 Field.IsImmutable 属性获取。

这样就可以轻松编写此类规则,例如:

1// <Name>Structures should be immutable</Name>
2
3warnif count > 0 from t in Application.Types where
4
5 t.IsStructure &&
6
7 !t.IsImmutable
8
9let mutableFields = t.Fields.Where(f => !f.IsImmutable)
10
11select new { t, t.NbLinesOfCode, mutableFields }

ChangesObjectState 和 ChangesTypeState 这两个属性可用于强制或检查某个方法是否 。一 方法是不分配任何实例或静态字段的方法。

此外,要控制对特定字段的写访问,您可以使用扩展方法 AssignField():

1from m in Methods where m.AssignField(&quot;MyClass.myfield&quot;)
2
3select new { m, m.NbLinesOfCode }

此外,CppDepend 为字段提供了 3 个属性 MethodsAssigningMe、MethodsReadingMeButNotAssigningMe 和 MethodsUsingMe,并为方法提供了 AssignField() 方法。

回到顶部

查询源文件路径

对于每个代码元素,您可以访问其 SourceDecls。访问源文件声明开启了一系列有趣的应用。例如,匹配要从……中丢弃的方法的默认查询 JustMyCode 代码库视图,依赖于源文件名的一些模式,并且可以轻松适应任何情况:

1// <Name>Discard generated and designer Methods from JustMyCode</Name>
2
3// --- Make sure to make this query richer to discard generated methods from CppDepend rules results ---
4
5notmycode
6
7//
8
9// First define source files paths to discard
10
11//
12
13from a in Application.Projects
14
15
16let projectSourceFilesPaths = a.SourceDecls.Select(s => s.SourceFile.FilePath)
17
18let sourceFilesPathsToDiscard = (
19
20 from filePath in projectSourceFilesPaths
21
22 let filePathLower= filePath.ToString().ToLower()
23
24 where
25
26 filePathLower.Contains(&quot;generated&quot;)
27
28 select filePath
29
30).ToHashSet()
31
32//
33
34// Second: discard methods in sourceFilesPathsToDiscard
35
36//
37
38from m in a.ChildMethods
39
40where (sourceFilesPathsToDiscard.Contains(m.SourceDecls.First().SourceFile.FilePath))
41
42select new { m, m.NbLinesOfCode }

创建新的 CppDepend 项目时,会提供多条关于源文件组织的默认规则。

立即试用 CppDepend

开始 14 天免费试用,畅享全部文档功能。无需信用卡。