Accueil/Docs/PRQL & Code Query/Fonctionnalités PRQL avancées

Fonctionnalités PRQL avancées

Fonctionnalités PRQL avancées

Cette section détaille les capacités avancées de PRQL (Code Query LINQ), le langage de requête de CppDepend pour analyser le code C/C++. De la requête sur la dette et les quality gates à l'exploration des dépendances de code et des conventions de nommage, PRQL offre de puissantes fonctionnalités pour une analyse approfondie du code.

Interroger la dette, les problèmes, les règles et les quality gates

Introduction

Depuis l'introduction de CppDepend v2017.1, PRQL ne sert pas seulement à interroger le code, mais aussi à interroger la dette, les problèmes, les règles et Quality gates.

Cette fonctionnalité est utile pour explorer en profondeur la dette technique. Dans documentation de la dette technique nous montrons comment quelques clics depuis le tableau de bord peuvent générer des requêtes pour explorer la dette et les problèmes.

Cette fonctionnalité est aussi utile pour définir métriques de tendance personnalisées, Quality Gates et Règles.

Par exemple, un Quality Gate qui définirait des seuils concernant le pourcentage de dette technique pourrait ressembler à :

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()

Une métrique de tendance qui compterait le nombre de règles critiques violées pourrait ressembler à :

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}

Non seulement cette métrique de tendance est utile pour suivre la tendance, mais son résultat est aussi navigable pour une exploration approfondie :

Métrique de tendance dans CppDepend

Interroger le diff depuis la baseline

Lorsqu'une baseline est disponible, les règles sont passées sur la baseline en plus d'être passées sur le snapshot actuel de la base de code. Ainsi, CppDepend peut comparer les deux ensembles de problèmes : l'ensemble obtenu en passant les règles sur la baseline et l'ensemble obtenu en passant les règles sur le snapshot actuel de la base de code.

PRQL peut ensuite être utilisé pour interroger les diffs de la dette, des problèmes, des règles et des Quality Gates. Par exemple, une métrique de tendance qui compte les nouveaux problèmes depuis la baseline pourrait ressembler à :

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 }

Un Quality Gate qui interdirait plus de 2 jours-homme de dette technique depuis la baseline pourrait ressembler à :

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()

Une douzaine de Quality Gates sont définis par défaut, et il est facile de les personnaliser et d'en créer de nouveaux. Dans la capture d'écran ci-dessous, cette requête (générée par un simple clic sur le Dashboard) montre non seulement le statut actuel des Quality Gates, mais aussi leur statut sur la baseline. Les Quality Gates qui reposent sur le diff ne peuvent pas être passés sur la baseline, c'est pourquoi ils ont un Non disponible (N/A) valeur.

quality gates CppDepend

De la même manière, de nombreuses métriques de tendance liées à la dette, aux problèmes, aux règles et aux Quality Gates sont définies par défaut, et il est facile de les personnaliser et d'en créer de nouvelles.

surveillance de tendance CppDepend

Le tableau de bord propose plusieurs menus pour générer des requêtes afin d'explorer le statut de la dette, des problèmes, des règles et des Quality Gates. Chaque nombre est également cliquable pour générer une requête qui liste les éléments comptés.

tableau de bord CppDepend

Comment ça marche

Des types spécialisés sont définis par CppDepend.API pour spécifier le modèle de dette, notamment Dette ; IIssue ; IRule ; IQualityGate ; QualityGateStatus.

Cependant, les deux types clés sont : IIssuesSet ; IIssuesSetDiff.

  • D'abord, CppDepend exécute les règles activées à la fois sur le snapshot actuel et sur la baseline.
  • Il calcule les problèmes, les chiffres de dette et les diffs.
  • Ensuite, il remplit ces objets issues-set et issues-set-diff.
  • Les requêtes qui reposent sur issues-set et issues-set-diff ne sont exécutées qu'une fois ces ensembles remplis. Par conséquent, une règle ne peut pas reposer sur ces ensembles, mais un Quality Gate le peut.

Au lieu d’écrire une requête comme...

1from i in context.IssuesSet.AllIssues select i

...ou comme...

1from i in context.IssuesSetDiff.OlderIssuesSet.AllIssues select i

...4 domaines sont proposés par PRQL : Problèmes, IssuesOnBaseline, Règles et QualityGates.

Ces domaines sont des raccourcis pour context.IssuesSet.AllIssues, context.IssuesSetDiff.OlderIssuesSet.AllIssues, context.IssuesSet.AllRules et context.IssuesSet.AllQualityGates.

Ces domaines peuvent être vus comme des variables de portée de type : IEnumerable ; IEnumerable ; IEnumerable.

Avec ces domaines, on peut alors écrire des requêtes simples comme...

1from i in Issues select i

...ou même juste :

Problèmes

De la même façon, au lieu de se référer constamment à issues-set et issues-set-diff pour obtenir des données comme par exemple...

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}

...les types IssuesSet et IssuesSetDiff propose des méthodes d'extension pratiques qui sont automatiquement traduites par le compilateur PRQL en appels à context.IssuesSet et context.IssuesSetDiff.

Par exemple, la requête ci-dessus peut être réécrite :

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}

La liste complète des méthodes d'extension proposées est :

  • HasIssue() / HasNewIssue() / HasIssueOnBaseline()
  • Debt() / NewDebt() / DebtOnBaseline()
  • AnnualInterest() / NewAnnualInterest() / AnnualInterestOnBaseline()
  • Issues() / NewerIssues() / IssuesOnBaseline()
  • RulesViolated() / RulesViolatedOnBaseline()
  • QualityGatesStatus() / QualityGatesStatusOnBaseline()

Notez qu’en travaillant avec une propriété comme AnnualInterest, le type Dette retournée définit une conversion implicite vers TimeSpan (car une dette est une certaine quantité de temps humain pour corriger). La sous-requête peut donc s'écrire :

1let annualInterest = Issues.Sum(i => i.AnnualInterest)

Ici annualInterest peut être de type Dette ou TimeSpan C'est également le cas pour une sous-expression comme :

1let debt = codeBase.TechnicalDebt
2
3let annualInterest = debt.AnnualInterest

...ce qui peut être réécrit :

1let annualInterest = codeBase.TechnicalDebt.AnnualInterest

Interroger le modèle objet de code

Chaque fois que vous écrivez une requête de code, vous écrivez une requête sur l'ensemble des types, méthodes et champs de votre modèle de base de code. Les éléments de code sont donc des objets centraux.

Voici une requête simple qui montre comment énumérer les classes de base d'une classe :

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

Et voici une requête qui montre comment énumérer les méthodes redéfinies par une méthode, et les méthodes qui redéfinissent une méthode :

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 }

Haut de page

Interroger les dépendances et la conception du code

Le modèle de dépendances est général au sens où il ne dépend pas du type d'utilisation (assignation de champ, appel de méthode...). Un et B étant deux éléments de code, nous disons que Un dépend de B si, quand B n’est pas disponible, Un ne peut pas être compilé.

Voici un exemple de requête de dépendances :

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

Notez comment ces méthodes d'extension sont utilisées par les règles générées à partir de graphe de dépendances ou matrice de dépendances, pour interdire une dépendance particulière :

interdire une dépendance dans CppDepend

La règle générée est présentée ci-dessous. Elle pourrait être facilement adaptée pour interdire ou imposer toute dépendance dans une base de code.

PRQL dans CppDepend

CppDepend fournit aussi un moyen pratique d'interroger les dépendances d'une base de code (et souvent un plus rapide de la même façon):

1from t in Types.UsedByAny(Types.Where(t => t.IsStatic)) select t

Utilisation indirecte

CppDepend fournit des méthodes contenant le mot Indirect dans leurs noms, pour gérer les dépendances indirectes comme par exemple la méthode IsIndirectlyUsing().

Par exemple si Un utilise B qui utilise C, Un n’est pas directement using C mais Un est indirectement using C (avec une profondeur de 2).

Voici une requête qui énumère les méthodes appelant directement ou indirectement la méthode Print MyClass.Print().

1from m in Methods
2
3 let depth0 = m.IsIndirectlyUsing("MyClass.Print()")
4
5select m

La méthode DepthOfIsUsing va plus loin car elle renvoie la profondeur d'utilisation.

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 }

La profondeur retournée est une Nullable valeur.

  • C’est null si m n’appelle pas indirectement la méthode cible.
  • C’est 1 si m appelle directement la méthode cible.
  • Il est supérieur à 1 si m appelle indirectement la méthode cible.

Cette possibilité d'utilisation indirecte est particulièrement utile pour générer Graphes d’appel ou Graphes d’héritage de classes.

Certaines méthodes contiennent aussi le mot Indirect ou le mot Profondeur pour travailler avec l'utilisation indirecte d'une séquence vers un élément de code ou même tout éléments de code d’une séquence cible (suffixe Tout).

Par exemple, la requête suivante correspond à toutes les méthodes qui appellent, directement ou indirectement les méthodes statiques, avec le profondeur d’appel :

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 }

Haut de page

Interroger la qualité du code et les métriques

CppDepend calcule plus de 80 métriques de code.

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 }

Notez que beaucoup de ces métriques de code retournent une valeur numérique nullable car elles ne sont pas nécessairement définies pour tous les éléments de code. Par exemple la #Lignes de code n’est pas calculé pour les éléments de code tiers.

Métriques personnalisées

Grâce à la flexibilité de LINQ, il est facile de composer les métriques de code par défaut pour créer des métriques plus élaborées, comme par exemple :

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 }

Un objet métrique de code personnalisé peut aussi être obtenu via la méthode FillIterative(). Par exemple, cette possibilité est utilisée dans la règle pour obtenir les types morts, mais aussi pour obtenir les types utilisés uniquement par des types morts (ainsi que la profondeur d'utilisation) :

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] }

Haut de page

Requêter le diff de code

CppDepend est livré avec la fonctionnalité unique de comparer deux instantanés différents d’une base de code pour voir ce qui a été changé/ajouté/supprimé.

Par exemple, la requête suivante énumère les méthodes dont le code a été modifié :

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

En fait, vous pouvez simplement écrire la requête la plus courte ci-dessous, et le compilateur PRQL se chargera de la transformer en la requête ci-dessus :

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

Une fois une telle requête écrite, l'UI CppDepend offre la possibilité de comparer les deux versions des fichiers sources de la méthode modifiée.

diff de méthodes dans CppDepend

Remarquez les deux méthodes OlderVersion() et NewerVersion(). Comme leurs noms le suggèrent, ces méthodes retournent la version plus ancienne ou plus récente d'un élément de code, ou null si l'élément de code a été ajouté (donc pas de version plus ancienne) ou supprimé (donc pas de version plus récente). Ces méthodes peuvent être utiles par exemple pour écrire la requête ci-dessous qui suit l'évolution en termes de complexité des méthodes :

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 }

Notez l’appel à IsPresentInBothBuilds pour s'assurer que nous ne traitons que des méthodes présentes à la fois dans l'ancien et le nouveau snapshot de la base de code, afin d'éviter tout NullReferenceException pendant l’exécution de la requête !

Comme le montre cette dernière requête, mélanger diff fonctionnalité avec d'autres fonctionnalités PRQL comme les métriques de qualité de code ou les dépendances, peut conduire à des requêtes et règles de code puissantes pour suivre l'évolution d'une base de code.

Haut de page

Interroger le nommage des éléments de code

PRQL propose plusieurs possibilités pour interroger le nom des éléments de code. C'est particulièrement utile pour écrire simple conventions de nommage du code (basées sur expressions régulières)...

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 }

...ou intelligente conventions de nommage du code :

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) }

La fonctionnalité de nommage des éléments de code est résumée dans le document de syntaxe PRQL, à la section Faire correspondre les éléments de code par nom.

Haut de page

Interroger la mutabilité des états

Le concept d'immutabilité devient de plus en plus populaire. L'immutabilité est particulièrement utile lorsqu'il s'agit d'accès concurrents dans un environnement multithread.

Un type est considéré comme immuable si ses champs d'instance ne peuvent plus être modifiés une fois l'instance construite par un constructeur. Un statique champ est considéré comme immuable s'il est privé et s'il n'est assigné que par le constructeur statique. Un instance champ est considéré comme immuable s'il est privé et s'il n'est assigné que par le ou les constructeurs de son type ou par son constructeur statique. Notez qu'un champ déclaré comme readonly est nécessairement immutable, mais un champ peut être immutable sans être déclaré comme readonly. Dans ce dernier cas, le mot-clé readonly peut être ajouté à la déclaration du champ, sans provoquer d'erreur de compilation.

Au moment de l'analyse, CppDepend calcule la mutabilité des types et des champs. Le résultat est disponible via les propriétés Type.IsImmutable et Field.IsImmutable.

Il est alors facile d'écrire une telle règle, par exemple :

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 }

Les deux propriétés ChangesObjectState et ChangesTypeState peuvent servir à imposer ou vérifier qu'une méthode est pure. Un pure méthode est une méthode qui n'assigne aucun champ d'instance ou statique.

De plus, pour contrôler l'accès en écriture à un champ particulier, vous pouvez utiliser la méthode d'extension AssignField() :

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

De plus, CppDepend fournit pour les champs les 3 propriétés MethodsAssigningMe, MethodsReadingMeButNotAssigningMe et MethodsUsingMe, et pour les méthodes, la méthode AssignField().

Haut de page

Interroger les chemins des fichiers sources

Pour chaque élément de code, vous pouvez accéder à ses SourceDecls. L'accès aux déclarations des fichiers sources ouvre un éventail d'applications intéressantes. Par exemple, la requête par défaut qui correspond aux méthodes à exclure du JustMyCode vue de base de code, s'appuie sur des motifs de nom de fichier source et peut être facilement adaptée à toute situation :

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 }

Plusieurs règles par défaut concernant l'organisation des fichiers sources sont proposées lors de la création d'un nouveau projet CppDepend.

Essayez CppDepend aujourd'hui

Commencez votre essai gratuit de 14 jours avec accès complet à toutes les fonctionnalités de documentation. Sans carte bancaire.