diff --git a/docs/_data/sidebars/pmd_sidebar.yml b/docs/_data/sidebars/pmd_sidebar.yml index 9587ef1247..7ea20c072a 100644 --- a/docs/_data/sidebars/pmd_sidebar.yml +++ b/docs/_data/sidebars/pmd_sidebar.yml @@ -156,3 +156,7 @@ entries: - title: Adding a New CPD Language url: /pmd_devdocs_adding_new_cpd_language.html output: web, pdf + - title: Adding metrics support to a language + url: /pmd_devdocs_adding_metrics_support_to_language.html + output: web, pdf + diff --git a/docs/pages/pmd/devdocs/adding_metrics_framework_for_language.md b/docs/pages/pmd/devdocs/adding_metrics_framework_for_language.md deleted file mode 100644 index 9811676bad..0000000000 --- a/docs/pages/pmd/devdocs/adding_metrics_framework_for_language.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: How to implement a metrics framework for an existing language -short_title: Implement a metrics framework -tags: [customizing] -summary: "PMD's Java module has an extensive framework for the calculation of metrics, which allows rule developers -to implement and use new code metrics very simply. Most of the functionality of this framework is abstracted in such -a way that any PMD supported language can implement such a framework without too much trouble. Here's how." -last_updated: July 3, 2016 -sidebar: pmd_sidebar -permalink: pmd_devdocs_adding_new_cpd_language.html -folder: pmd/devdocs ---- - -## Basic steps -* Implement the interface `QualifiedName` in a class. This implementation must be tailored to the target language so -that it can indentify unambiguously any class and operation in the analysed project (see JavaQualifiedName). -* Determine the AST nodes that correspond to class and method declaration in your language. These types are -referred hereafter as `T` and `O`, respectively. Both these types must implement the interface `QualifiableNode`, which -means they must provide a `getQualifiedName` method to give access to their qualified name. -* Implement the interface `Signature`, parameterized with the type of the method AST nodes. Method signatures -describe basic information about a method, which typically includes most of the modifiers they declare (eg -visibility, abstract or virtual, etc.). It's up to you to define the right level of detail, depending on the accuracy - of the pattern matching required. -* Make type `O` implement `SignedNode`. This makes the node capable of giving its signature. -* Create a class implementing `Memoizer` and one `Memoizer`. An abstract base class is available. Instances of -these classes each represent a class or operation, respectively. They are used to store the results of metrics that -are already computed. -* Create a class implementing `ProjectMirror`. This class will store the memoizers for all the classes and -interfaces of the analysed project. This class must be able to fetch and return a memoizer given the qualified name -of the resource it represents. As it stores the memoizers, it's a good idea to implement some signature matching -utilities in this class. What's signature matching? (See write custom metrics -- TODO) -* Create a class extending `AbstractMetricsComputer`. This object will be responsible for calculating metrics -given a memoizer, a node and info about the metric. Typically, this object is stateless so you might as well make it -a singleton. -* Create a class extending `AbstractMetricsFacade`. This class needs a reference to your `ProjectMirror` and -your `MetricsComputer`. It backs the real end user façade, and handles user provided parameters before delegating to -your `MetricsComputer`. -* Create the static façade of your framework. This one has an instance of your `MetricsFaçade` object and delegates -static methods to that instance. -* If you want to implement signature matching, create an `AbstractMetric` class, which gives access to a -`SignatureMatcher` to your metrics. Typically, your implementation of `ProjectMirror` implements a -custom `SignatureMatcher` interface, and your façade can give back its instance of the project mirror. -* Create classes `AbstractOperationMetric` and `AbstractClassMetric`. These must implement `Metric` and -`Metric`, respectively. They typically provide defaults for the `supports` method of each metric. -* Create enums `ClassMetricKey` and `OperationMetricKey`. These must implement `MetricKey` and `MetricKey`. The - enums list all available metric keys for your language. -* Create metrics by extending your base classes, reference them in your enums, and you can start using them with your - façade! \ No newline at end of file diff --git a/docs/pages/pmd/devdocs/adding_metrics_support_to_language.md b/docs/pages/pmd/devdocs/adding_metrics_support_to_language.md new file mode 100644 index 0000000000..2a299be86a --- /dev/null +++ b/docs/pages/pmd/devdocs/adding_metrics_support_to_language.md @@ -0,0 +1,124 @@ +--- +title: Adding support for metrics to a language +short_title: Implement a metrics framework +tags: [customizing] +summary: "PMD's Java module has an extensive framework for the calculation of metrics, which allows rule developers +to implement and use new code metrics very simply. Most of the functionality of this framework is abstracted in such +a way that any PMD supported language can implement such a framework without too much trouble. Here's how." +last_updated: August 2017 +sidebar: pmd_sidebar +permalink: pmd_devdocs_adding_metrics_support_to_language.html +folder: pmd/devdocs +--- + +{% include warning.html content="WIP" %} + +## Internal architecture of the metrics framework + +### Overview of the Java framework + +The framework has several subsystems, the two most easily identifiable being: +* The project mirror (`PackageStats`). This data structure gathers information about the classes, methods and fields of + the analysed project. It allows metrics to know about classes outside the current one, the files being processed one + by one. It's filled by a visitor before rules apply. + + The contents of the structure are indexed with fully qualified names (`JavaQualifiedName`), which must identify + unambiguously classes and methods. The information stored in this data structure that's accessible to metrics is + mainly comprised of method and field signatures (e.g. `JavaOperationSignature`), which describes concisely the + characteristics of the method or field (roughly, its modifiers). + + The project mirror is also responsible for the memoisation of metrics. When a metric is computed, it's stored back + in this structure and can be reused later. This reduces the overhead on the calculation of e.g. aggregate results + (`ResultOption` calculations). + +* The façade. The static end-user façade (`JavaMetrics`) is backed by an instance of a `JavaMetricsFaçade`. This + allows us to abstract the functionality of the façade into `pmd-core` for other frameworks to use. The façade + instance contains a project mirror, representing the analysed project, and a metrics computer + (`JavaMetricsComputer`). It's this last object which really computes the metric and stores back its result in the + project mirror, while the façade only handles parameters. + +Metrics (`Metric`) plug in to this static system and only provide behaviour that's executed by the metrics computer. +Internally, metric keys (`MetricKey`) are parameterized to their version (`MetricVersion`) to index memoisation maps +(see `ParameterizedMetricKey`). This allows us to memoise several versions of the same metric without conflict. + +### Abstraction layer + +As you may have seen, most of the functionality of the façade components has been abstracted into `pmd-core`. This +allows us to implement new metrics frameworks quite quickly. These abstract components are parameterized by the +node types of the class and operation AST nodes. + +The rest of the framework is framed by generic interfaces, but it can't really be abstracted more than that. For +instance, the project mirror is very language specific. Java's implementation uses the natural structure provided by +the language's package system to structure the project's content. Apex, on the other, has no package system and thus +can't use the same mechanism. That explains why the interfaces framing the project mirror are very loose. Their main +goal is to provide type safety through generics. + +Signature matching is another feature that couldn't be abstracted. For now, usage resolution depends on the availability + of type resolution for the given language, which is only implemented in java. We can however match signatures on the + class' own methods or nested classes, which offers limited interest, but may be useful. + +Despite these limitations, once the project mirror is implemented, it's very straightforward to get a working +framework. Additionnally, the external behaviour of the framework is very stable across languages, yet each component + can easily be customized by adding methods or overriding existing ones. + +## Implementation of a new framework + +### 1. Groundwork + +* Create a class implementing `QualifiedName`. This implementation must be tailored to the target language so +that it can indentify unambiguously any class and operation in the analysed project (see JavaQualifiedName). +* Determine the AST nodes that correspond to class and method declaration in your language. These types are +referred hereafter as `T` and `O`, respectively. Both these types must implement the interface `QualifiableNode`, which +means they must provide a `getQualifiedName` method to give access to their qualified name. + +### 2. Implement the project mirror +* Create a class implementing `Memoizer` and one `Memoizer`. An abstract base class is available. Instances of +these classes each represent a class or operation, respectively. They are used to store the results of metrics that +are already computed. +* Create a class implementing `ProjectMirror`. This class will store the memoizers for all the classes and +interfaces of the analysed project. This class must be able to fetch and return a memoizer given the qualified name +of the resource it represents. As it stores the memoizers, it's a good idea to implement some signature matching +utilities in this class. What's signature matching? (See write custom metrics -- TODO) + +### 3. Implement the façade +* Create a class extending `AbstractMetricsComputer`. This object will be responsible for calculating metrics +given a memoizer, a node and info about the metric. Typically, this object is stateless so you might as well make it +a singleton. +* Create a class extending `AbstractMetricsFacade`. This class needs a reference to your `ProjectMirror` and +your `MetricsComputer`. It backs the real end user façade, and handles user provided parameters before delegating to +your `MetricsComputer`. +* Create the static façade of your framework. This one has an instance of your `MetricsFaçade` object and delegates +static methods to that instance. +* Create classes `AbstractOperationMetric` and `AbstractClassMetric`. These must implement `Metric` and +`Metric`, respectively. They typically provide defaults for the `supports` method of each metric. +* Create enums `ClassMetricKey` and `OperationMetricKey`. These must implement `MetricKey` and `MetricKey`. The + enums list all available metric keys for your language. +* Create metrics by extending your base classes, reference them in your enums, and you can start using them with your + façade! + +### Optional: Signature matching + +You can match the signature of anything: method, field, class, package... It depends on what's useful for you. +Suppose you want to be able to match signatures for nodes of type `N`. What you have to do then is the following: + +* Create a class implementing the interface `Signature`. Signatures describe basic information about the node, +which typically includes most of the modifiers they declare (eg visibility, abstract or virtual, etc.). +It's up to you to define the right level of detail, depending on the accuracy of the pattern matching required. +* Make type `N` implement `SignedNode`. This makes the node capable of giving its signature. Factory methods to +build a `Signature` from a `N` are a good idea. +* Create signature masks. A mask is an object that matches some signatures based on their features. For example, with + the Java framework, you can build a `JavaOperationSigMask` that matches all method signatures with visibility + `public`. A sigmask implements `SigMask`, where `S` is the type of signature your mask handles. +* Typically, the project mirror stores the signatures, so you have to implement it in a way that makes it possible to + associate a signature with the qualified name of its node. + +{% include important.html +content="Writing this, it seems dumb. If signature matching is optional, it should not require reimplementing + the project mirror. We need to work on dissociating the two. The project mirror would be reduce to a + collection of memoizers, which could be abstracted into pmd-core." %} + + +* If you want to implement signature matching, create an `AbstractMetric` class, which gives access to a +`SignatureMatcher` to your metrics. Typically, your implementation of `ProjectMirror` implements a +custom `SignatureMatcher` interface, and your façade can give back its instance of the project mirror. + diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsComputer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsComputer.java index accf8c253f..336aa21386 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsComputer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsComputer.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import java.util.List; import net.sourceforge.pmd.lang.ast.QualifiableNode; -import net.sourceforge.pmd.lang.ast.SignedNode; /** * Base class for metrics computers. These objects compute a metric and memoize it. @@ -18,7 +17,7 @@ import net.sourceforge.pmd.lang.ast.SignedNode; * * @author Clément Fournier */ -public abstract class AbstractMetricsComputer & QualifiableNode> +public abstract class AbstractMetricsComputer implements MetricsComputer { @Override diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsFacade.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsFacade.java index f6f7984ef8..ca7b91c27a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsFacade.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/AbstractMetricsFacade.java @@ -5,7 +5,6 @@ package net.sourceforge.pmd.lang.metrics; import net.sourceforge.pmd.lang.ast.QualifiableNode; -import net.sourceforge.pmd.lang.ast.SignedNode; import net.sourceforge.pmd.lang.metrics.Metric.Version; /** @@ -17,7 +16,7 @@ import net.sourceforge.pmd.lang.metrics.Metric.Version; * * @author Clément Fournier */ -public abstract class AbstractMetricsFacade & QualifiableNode> { +public abstract class AbstractMetricsFacade { /** diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/MetricsComputer.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/MetricsComputer.java index e05edb9962..44eea26989 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/MetricsComputer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/MetricsComputer.java @@ -17,7 +17,7 @@ import net.sourceforge.pmd.lang.ast.SignedNode; * * @author Clément Fournier */ -public interface MetricsComputer & QualifiableNode> { +public interface MetricsComputer { /** diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/ProjectMirror.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/ProjectMirror.java index 7ed7e920f2..877d2d4260 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/ProjectMirror.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/ProjectMirror.java @@ -6,7 +6,6 @@ package net.sourceforge.pmd.lang.metrics; import net.sourceforge.pmd.lang.ast.QualifiableNode; import net.sourceforge.pmd.lang.ast.QualifiedName; -import net.sourceforge.pmd.lang.ast.SignedNode; /** * Object storing the statistics and memoizers of the analysed project, like PackageStats for Java. These are the entry @@ -28,7 +27,7 @@ import net.sourceforge.pmd.lang.ast.SignedNode; * * @author Clément Fournier */ -public interface ProjectMirror & QualifiableNode> { +public interface ProjectMirror { /** * Gets the operation metric memoizer corresponding to the qualified name. diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/SigMask.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/SigMask.java new file mode 100644 index 0000000000..b4ef01db10 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/metrics/SigMask.java @@ -0,0 +1,25 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.metrics; + +/** + * Generic signature mask. + * + * @param Type of signature this mask handles + * + * @author Clément Fournier + */ +public interface SigMask> { + + /** + * Returns true if the parameter is covered by this mask. + * + * @param sig The signature to test. + * + * @return True if the parameter is covered by this mask + */ + boolean covers(T sig); + +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/FieldSigMask.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/FieldSigMask.java index 3c7b86e5c2..cb4961c05f 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/FieldSigMask.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/FieldSigMask.java @@ -9,7 +9,7 @@ package net.sourceforge.pmd.lang.java.metrics.signature; * * @author Clément Fournier */ -public final class FieldSigMask extends SigMask { +public final class FieldSigMask extends JavaSigMask { private boolean coverFinal = true; private boolean coverStatic = true; diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaFieldSignature.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaFieldSignature.java index e2c9f01e15..27f0367f55 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaFieldSignature.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaFieldSignature.java @@ -15,7 +15,7 @@ import net.sourceforge.pmd.lang.metrics.Signature; * * @author Clément Fournier */ -public final class JavaFieldSignature extends JavaSignature implements Signature { +public final class JavaFieldSignature extends JavaSignature { private static final Map POOL = new HashMap<>(); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaOperationSignature.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaOperationSignature.java index bd17703cc3..144945ab9b 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaOperationSignature.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaOperationSignature.java @@ -18,7 +18,6 @@ import net.sourceforge.pmd.lang.java.ast.ASTResultType; import net.sourceforge.pmd.lang.java.ast.ASTType; import net.sourceforge.pmd.lang.java.symboltable.ClassScope; import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration; -import net.sourceforge.pmd.lang.metrics.Signature; import net.sourceforge.pmd.lang.symboltable.NameOccurrence; /** @@ -26,8 +25,7 @@ import net.sourceforge.pmd.lang.symboltable.NameOccurrence; * * @author Clément Fournier */ -public final class JavaOperationSignature extends JavaSignature - implements Signature { +public final class JavaOperationSignature extends JavaSignature { private static final Map POOL = new HashMap<>(); public final Role role; diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/SigMask.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSigMask.java similarity index 84% rename from pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/SigMask.java rename to pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSigMask.java index b427db2fe1..cc50cf8f50 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/SigMask.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSigMask.java @@ -9,6 +9,7 @@ import java.util.EnumSet; import java.util.Set; import net.sourceforge.pmd.lang.java.metrics.signature.JavaSignature.Visibility; +import net.sourceforge.pmd.lang.metrics.SigMask; /** * Generic signature mask. @@ -17,7 +18,7 @@ import net.sourceforge.pmd.lang.java.metrics.signature.JavaSignature.Visibility; * * @author Clément Fournier */ -public abstract class SigMask { +public abstract class JavaSigMask> implements SigMask { /** Visibility mask. */ private Set visMask = EnumSet.allOf(Visibility.class); @@ -52,13 +53,7 @@ public abstract class SigMask { } - /** - * Returns true if the parameter is covered by this mask. - * - * @param sig The signature to test. - * - * @return True if the parameter is covered by this mask - */ + @Override public boolean covers(T sig) { return visMask.contains(sig.visibility); } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSignature.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSignature.java index 722d643309..093a611461 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSignature.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/JavaSignature.java @@ -4,14 +4,16 @@ package net.sourceforge.pmd.lang.java.metrics.signature; +import net.sourceforge.pmd.lang.ast.SignedNode; import net.sourceforge.pmd.lang.java.ast.AccessNode; +import net.sourceforge.pmd.lang.metrics.Signature; /** * Generic signature. This class is extended by classes specific to operations and fields. * * @author Clément Fournier */ -public abstract class JavaSignature { +public abstract class JavaSignature> implements Signature { /** Visibility. */ public final Visibility visibility; diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/OperationSigMask.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/OperationSigMask.java index f74ada37f5..e74db2ae17 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/OperationSigMask.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/signature/OperationSigMask.java @@ -15,7 +15,7 @@ import net.sourceforge.pmd.lang.java.metrics.signature.JavaOperationSignature.Ro * * @author Clément Fournier */ -public final class OperationSigMask extends SigMask { +public final class OperationSigMask extends JavaSigMask { private Set roleMask = EnumSet.allOf(Role.class); private boolean coverAbstract = false; diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/SigMaskTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/JavaSigMaskTest.java similarity index 97% rename from pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/SigMaskTest.java rename to pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/JavaSigMaskTest.java index b2d62913bd..4e242472f7 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/SigMaskTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/JavaSigMaskTest.java @@ -22,12 +22,12 @@ import net.sourceforge.pmd.lang.java.metrics.signature.JavaOperationSignature; import net.sourceforge.pmd.lang.java.metrics.signature.JavaOperationSignature.Role; import net.sourceforge.pmd.lang.java.metrics.signature.JavaSignature.Visibility; import net.sourceforge.pmd.lang.java.metrics.signature.OperationSigMask; -import net.sourceforge.pmd.lang.java.metrics.signature.SigMask; +import net.sourceforge.pmd.lang.java.metrics.signature.JavaSigMask; /** * @author Clément Fournier */ -public class SigMaskTest extends ParserTst { +public class JavaSigMaskTest extends ParserTst { private static final String TEST_FIELDS = "class Bzaz{" + "public String x;" @@ -80,7 +80,7 @@ public class SigMaskTest extends ParserTst { @Test public void testEmptyOperationMask() { List nodes = getOrderedNodes(ASTMethodOrConstructorDeclaration.class, TEST_OPERATIONS); - SigMask mask = new OperationSigMask(); + JavaSigMask mask = new OperationSigMask(); for (ASTMethodOrConstructorDeclaration node : nodes) { if (node.isAbstract()) { @@ -97,7 +97,7 @@ public class SigMaskTest extends ParserTst { @Test public void testEmptyFieldMask() { List nodes = getOrderedNodes(ASTFieldDeclaration.class, TEST_FIELDS); - SigMask mask = new FieldSigMask(); + JavaSigMask mask = new FieldSigMask(); for (ASTFieldDeclaration node : nodes) { assertTrue(mask.covers(JavaFieldSignature.buildFor(node)));