diff --git a/1 b/1 new file mode 100644 index 000000000..e69de29bb diff --git a/docs/_data/sidebars/pmd_sidebar.yml b/docs/_data/sidebars/pmd_sidebar.yml index 361b1d838..efb51ff2b 100644 --- a/docs/_data/sidebars/pmd_sidebar.yml +++ b/docs/_data/sidebars/pmd_sidebar.yml @@ -162,3 +162,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_support_to_language.md b/docs/pages/pmd/devdocs/adding_metrics_support_to_language.md new file mode 100644 index 000000000..7773d1a1e --- /dev/null +++ b/docs/pages/pmd/devdocs/adding_metrics_support_to_language.md @@ -0,0 +1,142 @@ +--- +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, unstable API" %} + +## Internal architecture of the metrics framework + +### Overview of the Java framework + +The framework has several subsystems, the two most easily identifiable being: +* A **project memoizer** (`ProjectMemoizer`). 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 contents of + this data structure are indexed with fully qualified names (`JavaQualifiedName`), which must identify unambiguously + classes and methods. + +* 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 memoizer for 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 with their version (`MetricVersion`) to index memoisation +maps (see `ParameterizedMetricKey`). This allows us to memoise several versions of the same metric without conflict. + +{% include important.html content="The following will be moved when multifile analysis and metrics are separated" %} + + +At the very least, a metrics framework has those two components and is just a convenient way to compute and memoize +metrics on a single file. Yet, one of the goals of the metrics framework is to allow for **multi-file analysis**, which +make it possible, for instance, to compute the coupling between two classes. This feature uses two major +components: +* A **project mirror**. This data structure that stores info about all classes and operations (and other relevant + entities, such as fields, packages, etc.) of the analysed project. This is implemented by `PackageStats` in the Java + framework. The role of this structure is to make info about other files available to rules. It's filled by a visitor before rules apply. + + 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). + +* Some kind of method and field **usage resolution**, i.e. some way to find the fully qualified name of a method from a + method call expression node. This is the trickiest part to implement. In Java it depends on type resolution. + +### Abstraction layer + +As you may have seen, most of the functionality of the first two components are 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. Moreover, it makes 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. + +The signature matching aspect 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. + +Moreover, usage resolution depends on the availability of type resolution for the given language, which is only implemented in +Java. For these reasons, signature matching is considered an optional feature of the metrics framework. But despite +this limitation, signature matching still provides a elegant way to find information about the class we're in. This +feature requires no usage resolution and can be used to implement sophisticated metrics, that already give access to +detection strategies. + +## 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. You + must implement `equals`, `hashCode` and `toString`. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaQualifiedName.java) +* 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 expose a `getQualifiedName` method to give access to their qualified name. + +### 2. Implement and wire the project memoizer +* Create a class extending `BasicProjectMemoizer`. There's no abstract functionality to implement. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaProjectMemoizer.java) +* Create an AST visitor that fills the project memoizer with memoizers. For that, you use `BasicProjectMemoizer`'s + `addClassMemoizer` and `addOperationMemoizer` methods with a qualified name. + [Example](https://github.com/pmd/pmd/blob/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaMetricsVisitor.java) +* Create a façade class for your visitor. This class extends a `*ParserVisitorAdapter` class and only overrides the + `initializeWith(Node)` method. It's supposed to make your real visitor accept the node in parameter. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaMetricsVisitorFacade.java) +* Override the `getMetricsVisitorFacade()` method in your language's handler (e.g. `ApexHandler`). This method gives + back a `VisitorStarter` which initializes your façade with a `Node`. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/AbstractJavaHandler.java#L100-L108) +* Your project memoizer should now get filled when the `metrics` attribute is set to `true` in the rule XML. + +### 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. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaMetricsComputer.java) +* Create a class extending `AbstractMetricsFacade`. This class needs a reference to your `ProjectMemoizer` and + your `MetricsComputer`. It backs the real end user façade, and handles user provided parameters before delegating to + your `MetricsComputer`. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaMetricsFacade.java) +* 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. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/JavaMetrics.java) +* Create classes `AbstractOperationMetric` and `AbstractClassMetric`. These must implement `Metric` and + `Metric`, respectively. They typically provide defaults for the `supports` method of each metric. + [Example](https://github.com/pmd/pmd/blob/52d78d2fa97913cf73814d0307a1c1ae6125a437/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/impl/AbstractJavaOperationMetric.java) +* Create enums `ClassMetricKey` and `OperationMetricKey`. These must implement `MetricKey` and `MetricKey`. The + enums list all available metric keys for your language. + [Example](https://github.com/pmd/pmd/blob/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/metrics/api/JavaOperationMetricKey.java) +* Create metrics by extending your base classes, reference them in your enums, and you can start using them with your + façade! + +{% include important.html content="The following section will be moved when multifile analysis and metrics are separated" %} + +### 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. +* 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-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexHandler.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexHandler.java index 7a3df258f..92dd3d030 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexHandler.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexHandler.java @@ -13,6 +13,7 @@ import net.sourceforge.pmd.lang.XPathHandler; import net.sourceforge.pmd.lang.apex.ast.ApexNode; import net.sourceforge.pmd.lang.apex.ast.DumpFacade; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetricsVisitorFacade; import net.sourceforge.pmd.lang.apex.rule.ApexRuleViolationFactory; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.ast.xpath.AbstractASTXPathHandler; @@ -54,4 +55,14 @@ public void start(Node rootNode) { } }; } + + @Override + public VisitorStarter getMetricsVisitorFacade() { + return new VisitorStarter() { + @Override + public void start(Node rootNode) { + new ApexMetricsVisitorFacade().initializeWith((ApexNode) rootNode); + } + }; + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTMethod.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTMethod.java index 6445aab16..500191566 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTMethod.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTMethod.java @@ -4,9 +4,12 @@ package net.sourceforge.pmd.lang.apex.ast; +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSignature; +import net.sourceforge.pmd.lang.ast.SignedNode; + import apex.jorje.semantic.ast.member.Method; -public class ASTMethod extends AbstractApexNode { +public class ASTMethod extends AbstractApexNode implements ApexQualifiableNode, SignedNode { public ASTMethod(Method method) { super(method); @@ -40,4 +43,16 @@ public int getEndColumn() { return super.getEndColumn(); } + + + @Override + public ApexQualifiedName getQualifiedName() { + return ApexQualifiedName.ofMethod(this); + } + + + @Override + public ApexOperationSignature getSignature() { + return ApexOperationSignature.of(this); + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClass.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClass.java index b0c83a2fe..c506ce13c 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClass.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClass.java @@ -9,16 +9,21 @@ import apex.jorje.data.ast.Identifier; import apex.jorje.semantic.ast.compilation.UserClass; -public class ASTUserClass extends ApexRootNode { +public class ASTUserClass extends ApexRootNode implements ASTUserClassOrInterface { + + private ApexQualifiedName qname; + public ASTUserClass(UserClass userClass) { super(userClass); } + public Object jjtAccept(ApexParserVisitor visitor, Object data) { return visitor.visit(this, data); } + @Override public String getImage() { try { @@ -31,4 +36,27 @@ public String getImage() { } return super.getImage(); } + + + @Override + public ApexQualifiedName getQualifiedName() { + if (qname == null) { + + ASTUserClass parent = this.getFirstParentOfType(ASTUserClass.class); + + if (parent != null) { + qname = ApexQualifiedName.ofNestedClass(parent.getQualifiedName(), this); + } else { + qname = ApexQualifiedName.ofOuterClass(this); + } + } + + return qname; + } + + + @Override + public TypeKind getTypeKind() { + return TypeKind.CLASS; + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClassOrInterface.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClassOrInterface.java new file mode 100644 index 000000000..467656d13 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserClassOrInterface.java @@ -0,0 +1,30 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import apex.jorje.semantic.ast.AstNode; + +/** + * @author Clément Fournier + */ +public interface ASTUserClassOrInterface extends ApexQualifiableNode, ApexNode { + + /** + * Finds the type kind of this declaration. + * + * @return The type kind of this declaration. + */ + TypeKind getTypeKind(); + + + /** + * The kind of type this node declares. + */ + enum TypeKind { + CLASS, INTERFACE + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserInterface.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserInterface.java index 4e15e7426..f9b8a181b 100644 --- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserInterface.java +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTUserInterface.java @@ -9,7 +9,9 @@ import apex.jorje.data.ast.Identifier; import apex.jorje.semantic.ast.compilation.UserInterface; -public class ASTUserInterface extends ApexRootNode { +public class ASTUserInterface extends ApexRootNode implements ASTUserClassOrInterface { + + private ApexQualifiedName qname; public ASTUserInterface(UserInterface userInterface) { super(userInterface); @@ -31,4 +33,27 @@ public String getImage() { } return super.getImage(); } + + + @Override + public TypeKind getTypeKind() { + return TypeKind.INTERFACE; + } + + + @Override + public ApexQualifiedName getQualifiedName() { + if (qname == null) { + + ASTUserClass parent = this.getFirstParentOfType(ASTUserClass.class); + + if (parent != null) { + qname = ApexQualifiedName.ofNestedClass(parent.getQualifiedName(), this); + } else { + qname = ApexQualifiedName.ofOuterClass(this); + } + } + + return qname; + } } diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorReducedAdapter.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorReducedAdapter.java new file mode 100644 index 000000000..f989a95bd --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexParserVisitorReducedAdapter.java @@ -0,0 +1,29 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +/** + * @author Clément Fournier + */ +public class ApexParserVisitorReducedAdapter extends ApexParserVisitorAdapter { + + + @Override + public final Object visit(ASTUserInterface node, Object data) { + return visit((ASTUserClassOrInterface) node, data); + } + + + @Override + public final Object visit(ASTUserClass node, Object data) { + return visit((ASTUserClassOrInterface) node, data); + } + + + public Object visit(ASTUserClassOrInterface node, Object data) { + return visit((ApexNode) node, data); + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiableNode.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiableNode.java new file mode 100644 index 000000000..13cfb5134 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiableNode.java @@ -0,0 +1,16 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import net.sourceforge.pmd.lang.ast.QualifiableNode; + +/** + * @author Clément Fournier + */ +public interface ApexQualifiableNode extends QualifiableNode { + + @Override + ApexQualifiedName getQualifiedName(); +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedName.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedName.java new file mode 100644 index 000000000..f65605d0a --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedName.java @@ -0,0 +1,177 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import net.sourceforge.pmd.lang.ast.QualifiedName; + +import apex.jorje.semantic.symbol.type.TypeInfo; + +/** + * Qualified name of an apex class or method. + * + * @author Clément Fournier + */ +public class ApexQualifiedName implements QualifiedName { + + + private final String nameSpace; + private final String[] classes; + private final String operation; + + + private ApexQualifiedName(String nameSpace, String[] classes, String operation) { + this.nameSpace = nameSpace; + this.operation = operation; + this.classes = classes; + } + + + public String getOperation() { + return operation; + } + + + public String[] getClasses() { + return Arrays.copyOf(classes, classes.length); + } + + + /** + * Gets the namespace prefix of this resource. + * + * @return The namespace prefix + */ + public String getNameSpace() { + return nameSpace; + } + + + @Override + public boolean isClass() { + return operation == null; + } + + + @Override + public boolean isOperation() { + return operation != null; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(nameSpace).append("__"); + sb.append(classes[0]); + + if (classes.length > 1) { + sb.append('.').append(classes[1]); + } + + if (isOperation()) { + sb.append("#").append(operation); + } + + return sb.toString(); + } + + + @Override + public ApexQualifiedName getClassName() { + if (isClass()) { + return this; + } + + return new ApexQualifiedName(this.nameSpace, this.classes, null); + } + + + @Override + public int hashCode() { + int result = nameSpace.hashCode(); + result = 31 * result + Arrays.hashCode(classes); + result = 31 * result + (operation != null ? operation.hashCode() : 0); + return result; + } + + + @Override + public boolean equals(Object obj) { + return obj instanceof ApexQualifiedName + && Objects.deepEquals(classes, ((ApexQualifiedName) obj).classes) + && Objects.equals(operation, ((ApexQualifiedName) obj).operation) + && Objects.equals(nameSpace, ((ApexQualifiedName) obj).nameSpace); + + } + + + /** + * Parses a string conforming to the format defined below and returns an ApexQualifiedName. + * + *

Here are some examples of the format: + *

    + *
  • {@code namespace__OuterClass.InnerClass}: name of an inner class + *
  • {@code namespace__Class#method(String, int)}: name of an operation + *
+ * + * @param toParse The string to parse + * + * @return An ApexQualifiedName, or null if the string couldn't be parsed + */ + // private static final Pattern FORMAT = Pattern.compile("(\\w+)__(\\w+)(.(\\w+))?(#(\\w+))?"); // TODO + public static ApexQualifiedName ofString(String toParse) { + throw new UnsupportedOperationException(); + } + + + static ApexQualifiedName ofOuterClass(ASTUserClassOrInterface astUserClass) { + String ns = astUserClass.getNode().getDefiningType().getNamespace().toString(); + String[] classes = {astUserClass.getImage()}; + return new ApexQualifiedName(StringUtils.isEmpty(ns) ? "c" : ns, classes, null); + } + + + static ApexQualifiedName ofNestedClass(ApexQualifiedName parent, ASTUserClassOrInterface astUserClass) { + + String[] classes = Arrays.copyOf(parent.classes, parent.classes.length + 1); + classes[classes.length - 1] = astUserClass.getImage(); + return new ApexQualifiedName(parent.nameSpace, classes, null); + } + + + private static String getOperationString(ASTMethod node) { + StringBuilder sb = new StringBuilder(); + sb.append(node.getImage()).append('('); + + + List paramTypes = node.getNode().getMethodInfo().getParameterTypes(); + + if (paramTypes.size() > 0) { + sb.append(paramTypes.get(0).getApexName()); + + for (int i = 1; i < paramTypes.size(); i++) { + sb.append(",").append(paramTypes.get(i).getApexName()); + } + + } + + sb.append(')'); + + return sb.toString(); + } + + + static ApexQualifiedName ofMethod(ASTMethod node) { + ApexQualifiedName parent = node.getFirstParentOfType(ASTUserClassOrInterface.class).getQualifiedName(); + + return new ApexQualifiedName(parent.nameSpace, parent.classes, getOperationString(node)); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/AbstractApexMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/AbstractApexMetric.java new file mode 100644 index 000000000..f093f0b5b --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/AbstractApexMetric.java @@ -0,0 +1,18 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +/** + * Base class for all Apex metrics. + * + * @author Clément Fournier + */ +public class AbstractApexMetric { + + protected ApexSignatureMatcher getSignatureMatcher() { + return ApexMetrics.getFacade().getProjectMirror(); + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexClassStats.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexClassStats.java new file mode 100644 index 000000000..6619dafec --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexClassStats.java @@ -0,0 +1,44 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSigMask; +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSignature; + +/** + * Stores info about a class. + * + * @author Clément Fournier + */ +class ApexClassStats { + + private Map> operations = new HashMap<>(); + + + void addOperation(String name, ApexOperationSignature sig) { + if (!operations.containsKey(sig)) { + operations.put(sig, new HashSet<>()); + } + operations.get(sig).add(name); + } + + + public boolean hasMatchingSig(String operation, ApexOperationSigMask mask) { + for (Entry> entry : operations.entrySet()) { + if (mask.covers(entry.getKey())) { + if (entry.getValue().contains(operation)) { + return true; + } + } + } + return false; + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetrics.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetrics.java new file mode 100644 index 000000000..080ae22d0 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetrics.java @@ -0,0 +1,135 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.metrics.Metric.Version; +import net.sourceforge.pmd.lang.metrics.MetricKey; +import net.sourceforge.pmd.lang.metrics.MetricVersion; +import net.sourceforge.pmd.lang.metrics.ResultOption; + +/** + * User-bound façade of the Apex metrics framework. + * + * @author Clément Fournier + */ +public final class ApexMetrics { + + private static final ApexMetricsFacade FACADE = new ApexMetricsFacade(); + + + private ApexMetrics() { // Cannot be instantiated + + } + + + /** + * Returns the underlying facade. + * + * @return The facade + */ + public static ApexMetricsFacade getFacade() { + return FACADE; + } + + + /** Resets the entire data structure. Used for tests. */ + static void reset() { + FACADE.reset(); + } + + + /** + * Computes the standard value of the metric identified by its code on a class AST node. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed + */ + public static double get(MetricKey> key, ASTUserClass node) { + return FACADE.computeForType(key, node, Version.STANDARD); + } + + + /** + * Computes a metric identified by its code on a class AST node, possibly selecting a variant with the {@code + * MetricVersion} parameter. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * @param version The version of the metric + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed + */ + public static double get(MetricKey> key, ASTUserClass node, MetricVersion version) { + return FACADE.computeForType(key, node, version); + } + + + /** + * Computes the standard version of the metric identified by the key on a operation AST node. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed + */ + public static double get(MetricKey key, ASTMethod node) { + return FACADE.computeForOperation(key, node, Version.STANDARD); + } + + + /** + * Computes a metric identified by its key on a operation AST node. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * @param version The version of the metric + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed + */ + public static double get(MetricKey key, ASTMethod node, MetricVersion version) { + return FACADE.computeForOperation(key, node, version); + } + + + /** + * Compute the sum, average, or highest value of the standard operation metric on all operations of the class node. + * The type of operation is specified by the {@link ResultOption} parameter. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * @param option The result option to use + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed or {@code option} is + * {@code null} + */ + public static double get(MetricKey key, ASTUserClassOrInterface node, ResultOption option) { + return FACADE.computeWithResultOption(key, node, Version.STANDARD, option); + } + + + /** + * Compute the sum, average, or highest value of the operation metric on all operations of the class node. The type + * of operation is specified by the {@link ResultOption} parameter. + * + * @param key The key identifying the metric to be computed + * @param node The node on which to compute the metric + * @param version The version of the metric + * @param option The result option to use + * + * @return The value of the metric, or {@code Double.NaN} if the value couln't be computed or {@code option} is + * {@code null} + */ + public static double get(MetricKey key, ASTUserClassOrInterface node, MetricVersion version, + ResultOption option) { + return FACADE.computeWithResultOption(key, node, version, option); + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsComputer.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsComputer.java new file mode 100644 index 000000000..d0d82c3d6 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsComputer.java @@ -0,0 +1,35 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.metrics.AbstractMetricsComputer; + +/** + * Computes metrics for the Apex framework. + * + * @author Clément Fournier + */ +public class ApexMetricsComputer extends AbstractMetricsComputer, ASTMethod> { + + static final ApexMetricsComputer INSTANCE = new ApexMetricsComputer(); + + + @Override + protected List findOperations(ASTUserClassOrInterface node) { + List candidates = node.findChildrenOfType(ASTMethod.class); + List result = new ArrayList<>(candidates); + for (ASTMethod method : candidates) { + if (method.getImage().matches("(||clone)")) { + result.remove(method); + } + } + return result; + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsFacade.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsFacade.java new file mode 100644 index 000000000..8c22a7e84 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsFacade.java @@ -0,0 +1,49 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.metrics.AbstractMetricsFacade; +import net.sourceforge.pmd.lang.metrics.MetricsComputer; + +/** + * Backs the static façade. + * @author Clément Fournier + */ +public class ApexMetricsFacade extends AbstractMetricsFacade, ASTMethod> { + + private final ApexProjectMirror projectMirror = new ApexProjectMirror(); + private final ApexProjectMemoizer memoizer = new ApexProjectMemoizer(); + + + /** Resets the entire project mirror. Used for tests. */ + void reset() { + projectMirror.reset(); + memoizer.reset(); + } + + + /** + * Gets the project mirror. + * + * @return The project mirror + */ + public ApexProjectMirror getProjectMirror() { + return projectMirror; + } + + + @Override + protected MetricsComputer, ASTMethod> getLanguageSpecificComputer() { + return ApexMetricsComputer.INSTANCE; + } + + + @Override + protected ApexProjectMemoizer getLanguageSpecificProjectMemoizer() { + return memoizer; + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitor.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitor.java new file mode 100644 index 000000000..cfa0b8e30 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitor.java @@ -0,0 +1,50 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import java.util.Stack; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorReducedAdapter; + +/** + * @author Clément Fournier + */ +public class ApexMetricsVisitor extends ApexParserVisitorReducedAdapter { + + private final ApexProjectMemoizer memoizer; + private final ApexProjectMirror mirror; + + private final Stack stack = new Stack<>(); + + + public ApexMetricsVisitor(ApexProjectMemoizer memoizer, ApexProjectMirror mirror) { + this.memoizer = memoizer; + this.mirror = mirror; + } + + @Override + public Object visit(ASTUserClassOrInterface node, Object data) { + memoizer.addClassMemoizer(node.getQualifiedName()); + stack.push(mirror.getClassStats(node.getQualifiedName(), true)); + super.visit(node, data); + stack.pop(); + + return data; + } + + + + + @Override + public Object visit(ASTMethod node, Object data) { + memoizer.addOperationMemoizer(node.getQualifiedName()); + + stack.peek().addOperation(node.getQualifiedName().getOperation(), node.getSignature()); + return data; + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorFacade.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorFacade.java new file mode 100644 index 000000000..78dbcd687 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorFacade.java @@ -0,0 +1,23 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; + +/** + * @author Clément Fournier + */ +public class ApexMetricsVisitorFacade extends ApexParserVisitorAdapter { + + + public void initializeWith(ApexNode rootNode) { + ApexMetricsFacade facade = ApexMetrics.getFacade(); + ApexMetricsVisitor visitor = new ApexMetricsVisitor(facade.getLanguageSpecificProjectMemoizer(), + facade.getProjectMirror()); + rootNode.jjtAccept(visitor, null); + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMemoizer.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMemoizer.java new file mode 100644 index 000000000..6e71251ee --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMemoizer.java @@ -0,0 +1,17 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.metrics.BasicProjectMemoizer; + +/** + * Memoizer for Apex metrics. + * + * @author Clément Fournier + */ +class ApexProjectMemoizer extends BasicProjectMemoizer, ASTMethod> { +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirror.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirror.java new file mode 100644 index 000000000..a747db733 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirror.java @@ -0,0 +1,46 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import java.util.HashMap; +import java.util.Map; + +import net.sourceforge.pmd.lang.apex.ast.ApexQualifiedName; +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSigMask; + +/** + * Equivalent to PackageStats in the java framework. + * + * @author Clément Fournier + */ +public class ApexProjectMirror implements ApexSignatureMatcher { + + private final Map classes = new HashMap<>(); + + + void reset() { + classes.clear(); + } + + + ApexClassStats getClassStats(ApexQualifiedName qname, boolean createIfNotFound) { + ApexQualifiedName className = qname.getClassName(); + if (createIfNotFound && !classes.containsKey(className)) { + classes.put(className, new ApexClassStats()); + } + return classes.get(className); + } + + + @Override + public boolean hasMatchingSig(ApexQualifiedName qname, ApexOperationSigMask mask) { + ApexClassStats classStats = getClassStats(qname, false); + + return classStats != null && classStats.hasMatchingSig(qname.getOperation(), mask); + + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexSignatureMatcher.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexSignatureMatcher.java new file mode 100644 index 000000000..aa45ea9c2 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/ApexSignatureMatcher.java @@ -0,0 +1,24 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import net.sourceforge.pmd.lang.apex.ast.ApexQualifiedName; +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSigMask; + +/** + * @author Clément Fournier + */ +public interface ApexSignatureMatcher { + + /** + * Returns true if the signature of the operation designated by the qualified name is covered by the mask. + * + * @param qname The operation to test + * @param sigMask The signature mask to use + * + * @return True if the signature of the operation designated by the qualified name is covered by the mask + */ + boolean hasMatchingSig(ApexQualifiedName qname, ApexOperationSigMask sigMask); +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetric.java new file mode 100644 index 000000000..83bb14286 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetric.java @@ -0,0 +1,14 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.api; + +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.metrics.Metric; + +/** + * @author Clément Fournier + */ +public interface ApexClassMetric extends Metric> { +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java new file mode 100644 index 000000000..2ffae23a4 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexClassMetricKey.java @@ -0,0 +1,37 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.api; + +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.metrics.impl.WmcMetric; +import net.sourceforge.pmd.lang.metrics.MetricKey; + +/** + * @author Clément Fournier + */ +public enum ApexClassMetricKey implements MetricKey> { + WMC(new WmcMetric()); + + + private final ApexClassMetric calculator; + + + ApexClassMetricKey(ApexClassMetric m) { + calculator = m; + } + + + @Override + public ApexClassMetric getCalculator() { + return calculator; + } + + + @Override + public boolean supports(ASTUserClassOrInterface node) { + return calculator.supports(node); + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetric.java new file mode 100644 index 000000000..79ed8323c --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetric.java @@ -0,0 +1,14 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.api; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.metrics.Metric; + +/** + * @author Clément Fournier + */ +public interface ApexOperationMetric extends Metric { +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java new file mode 100644 index 000000000..502dbbd06 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/api/ApexOperationMetricKey.java @@ -0,0 +1,36 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.api; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.metrics.impl.CycloMetric; +import net.sourceforge.pmd.lang.metrics.MetricKey; + +/** + * @author Clément Fournier + */ +public enum ApexOperationMetricKey implements MetricKey { + CYCLO(new CycloMetric()); + + + private final ApexOperationMetric calculator; + + + ApexOperationMetricKey(ApexOperationMetric m) { + calculator = m; + } + + + @Override + public ApexOperationMetric getCalculator() { + return calculator; + } + + + @Override + public boolean supports(ASTMethod node) { + return calculator.supports(node); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexClassMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexClassMetric.java new file mode 100644 index 000000000..02d6588a5 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexClassMetric.java @@ -0,0 +1,23 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface.TypeKind; +import net.sourceforge.pmd.lang.apex.metrics.AbstractApexMetric; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetric; + +/** + * Base class for Apex metrics. + * + * @author Clément Fournier + */ +public abstract class AbstractApexClassMetric extends AbstractApexMetric implements ApexClassMetric { + + @Override + public boolean supports(ASTUserClassOrInterface node) { + return node.getTypeKind() == TypeKind.CLASS; + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexOperationMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexOperationMetric.java new file mode 100644 index 000000000..ad8a1e54d --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexOperationMetric.java @@ -0,0 +1,32 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode; +import net.sourceforge.pmd.lang.apex.metrics.AbstractApexMetric; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetric; + +/** + * Base class for Apex operation metrics. + * + * @author Clément Fournier + */ +public abstract class AbstractApexOperationMetric extends AbstractApexMetric implements ApexOperationMetric { + + /** + * Checks if the metric can be computed on the node. For now, we filter out {@literal , and clone}, + * which are present in all apex class nodes even if they're not implemented, which may yield unexpected results. + * + * @param node The node to check + * + * @return True if the metric can be computed + */ + @Override + public boolean supports(ASTMethod node) { + return !node.getImage().matches("(||clone)") + && !node.getFirstChildOfType(ASTModifierNode.class).isAbstract(); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloMetric.java new file mode 100644 index 000000000..1946dfa24 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloMetric.java @@ -0,0 +1,55 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.mutable.MutableInt; + +import net.sourceforge.pmd.lang.apex.ast.ASTBooleanExpression; +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTStandardCondition; +import net.sourceforge.pmd.lang.apex.metrics.impl.visitors.StandardCycloVisitor; +import net.sourceforge.pmd.lang.metrics.MetricVersion; + +import apex.jorje.data.ast.BooleanOp; + +/** + * See the doc for the Java metric. + * + * @author Clément Fournier + */ +public class CycloMetric extends AbstractApexOperationMetric { + + + @Override + public double computeFor(ASTMethod node, MetricVersion version) { + return ((MutableInt) node.jjtAccept(new StandardCycloVisitor(), new MutableInt(1))).doubleValue(); + } + + + /** + * Computes the number of control flow paths through that expression, which is the number of {@code ||} and {@code + * &&} operators. Used both by Npath and Cyclo. + * + * @param expression Boolean expression + * + * @return The complexity of the expression + */ + public static int booleanExpressionComplexity(ASTStandardCondition expression) { + Set subs = new HashSet<>(expression.findDescendantsOfType(ASTBooleanExpression.class)); + int complexity = 0; + + for (ASTBooleanExpression sub : subs) { + BooleanOp op = sub.getNode().getOp(); + if (op != null && (op == BooleanOp.AND || op == BooleanOp.OR)) { + complexity++; + } + } + + return complexity; + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcMetric.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcMetric.java new file mode 100644 index 000000000..136dad1d5 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcMetric.java @@ -0,0 +1,24 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetrics; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; +import net.sourceforge.pmd.lang.metrics.MetricVersion; +import net.sourceforge.pmd.lang.metrics.ResultOption; + +/** + * See the doc for the Java metric. + * + * @author Clément Fournier + */ +public class WmcMetric extends AbstractApexClassMetric { + + @Override + public double computeFor(ASTUserClassOrInterface node, MetricVersion version) { + return ApexMetrics.get(ApexOperationMetricKey.CYCLO, node, ResultOption.SUM); + } +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/StandardCycloVisitor.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/StandardCycloVisitor.java new file mode 100644 index 000000000..001fda93d --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/impl/visitors/StandardCycloVisitor.java @@ -0,0 +1,101 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl.visitors; + +import org.apache.commons.lang3.mutable.MutableInt; + +import net.sourceforge.pmd.lang.apex.ast.ASTCatchBlockStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTDoLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTForEachStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTForLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTIfBlockStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTStandardCondition; +import net.sourceforge.pmd.lang.apex.ast.ASTTernaryExpression; +import net.sourceforge.pmd.lang.apex.ast.ASTThrowStatement; +import net.sourceforge.pmd.lang.apex.ast.ASTWhileLoopStatement; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; +import net.sourceforge.pmd.lang.apex.metrics.impl.CycloMetric; + +/** + * @author Clément Fournier + */ +public class StandardCycloVisitor extends ApexParserVisitorAdapter { + + @Override + public Object visit(ASTMethod node, Object data) { + return super.visit(node, data); + } + + + @Override + public Object visit(ASTIfBlockStatement node, Object data) { + ((MutableInt) data).add( + 1 + CycloMetric.booleanExpressionComplexity(node.getFirstDescendantOfType(ASTStandardCondition.class))); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTCatchBlockStatement node, Object data) { + ((MutableInt) data).increment(); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTForLoopStatement node, Object data) { + ((MutableInt) data).add( + 1 + CycloMetric.booleanExpressionComplexity(node.getFirstDescendantOfType(ASTStandardCondition.class))); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTForEachStatement node, Object data) { + ((MutableInt) data).increment(); + super.visit(node, data); + return data; + } + + @Override + public Object visit(ASTThrowStatement node, Object data) { + ((MutableInt) data).increment(); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTWhileLoopStatement node, Object data) { + ((MutableInt) data).add( + 1 + CycloMetric.booleanExpressionComplexity(node.getFirstDescendantOfType(ASTStandardCondition.class))); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTDoLoopStatement node, Object data) { + ((MutableInt) data).add( + 1 + CycloMetric.booleanExpressionComplexity(node.getFirstDescendantOfType(ASTStandardCondition.class))); + super.visit(node, data); + return data; + } + + + @Override + public Object visit(ASTTernaryExpression node, Object data) { + ((MutableInt) data).add( + 1 + CycloMetric.booleanExpressionComplexity(node.getFirstDescendantOfType(ASTStandardCondition.class))); + super.visit(node, data); + return data; + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/rule/CyclomaticComplexityRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/rule/CyclomaticComplexityRule.java new file mode 100644 index 000000000..9b3d0736d --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/rule/CyclomaticComplexityRule.java @@ -0,0 +1,81 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.rule; + + +import java.util.Stack; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetrics; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; +import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule; +import net.sourceforge.pmd.lang.metrics.ResultOption; +import net.sourceforge.pmd.lang.rule.properties.IntegerProperty; + +/** + * Cyclomatic complexity rule using metrics. Uses Wmc to report classes (the Java rule will be updated as well in an + * upcoming PR) + * + * @author Clément Fournier + */ +public class CyclomaticComplexityRule extends AbstractApexRule { + + private static final IntegerProperty CLASS_LEVEL_DESCRIPTOR = new IntegerProperty( + "classReportLevel", "Total class complexity reporting threshold", 1, 200, 40, 1.0f); + + private static final IntegerProperty METHOD_LEVEL_DESCRIPTOR = new IntegerProperty( + "methodReportLevel", "Cyclomatic complexity reporting threshold", 1, 30, 10, 1.0f); + + Stack classNames = new Stack<>(); + + + public CyclomaticComplexityRule() { + definePropertyDescriptor(CLASS_LEVEL_DESCRIPTOR); + definePropertyDescriptor(METHOD_LEVEL_DESCRIPTOR); + } + + + @Override + public Object visit(ASTUserClass node, Object data) { + + classNames.push(node.getImage()); + super.visit(node, data); + classNames.pop(); + + if (ApexClassMetricKey.WMC.supports(node)) { + int classWmc = (int) ApexMetrics.get(ApexClassMetricKey.WMC, node); + + if (classWmc >= getProperty(CLASS_LEVEL_DESCRIPTOR)) { + int classHighest = (int) ApexMetrics.get(ApexOperationMetricKey.CYCLO, node, ResultOption.HIGHEST); + + String[] messageParams = {"class", + node.getImage(), + " total", + classWmc + " (highest " + classHighest + ")", }; + + addViolation(data, node, messageParams); + } + } + return data; + } + + + @Override + public final Object visit(ASTMethod node, Object data) { + + int cyclo = (int) ApexMetrics.get(ApexOperationMetricKey.CYCLO, node); + if (cyclo >= getProperty(METHOD_LEVEL_DESCRIPTOR)) { + addViolation(data, node, new String[] {node.getImage().equals(classNames.peek()) ? "constructor" : "method", + node.getQualifiedName().getOperation(), + "", + "" + cyclo, }); + } + + return data; + } + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSigMask.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSigMask.java new file mode 100644 index 000000000..382309374 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSigMask.java @@ -0,0 +1,66 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.signature; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; + +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexSignature.Visibility; + +/** + * @author Clément Fournier + */ +public class ApexOperationSigMask { + + private Set visMask = EnumSet.allOf(Visibility.class); + + + public ApexOperationSigMask() { + } + + + /** + * Sets the mask to cover all visibilities. + */ + public void coverAllVisibilities() { + visMask.addAll(Arrays.asList(Visibility.values())); + } + + + /** + * Restricts the visibilities covered by the mask to the parameters. + * + * @param visibilities The visibilities to cover + */ + public void restrictVisibilitiesTo(Visibility... visibilities) { + visMask.clear(); + visMask.addAll(Arrays.asList(visibilities)); + } + + + /** + * Forbid all mentioned visibilities. + * + * @param visibilities The visibilities to forbid + */ + public void forbid(Visibility... visibilities) { + visMask.removeAll(Arrays.asList(visibilities)); + } + + + /** + * 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 + */ + public boolean covers(ApexOperationSignature sig) { + return visMask.contains(sig.visibility); + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSignature.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSignature.java new file mode 100644 index 000000000..3d2206f8a --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexOperationSignature.java @@ -0,0 +1,65 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.signature; + +import java.util.HashMap; +import java.util.Map; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.metrics.Signature; + +/** + * @author Clément Fournier + */ +public final class ApexOperationSignature extends ApexSignature implements Signature { + + private static final Map POOL = new HashMap<>(); + + + /** + * Create a signature using its visibility. + * + * @param visibility The visibility + */ + private ApexOperationSignature(Visibility visibility) { + super(visibility); + } + + + @Override + public int hashCode() { + return code(visibility); + } + + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + + private static int code(Visibility visibility) { + return visibility.hashCode(); + } + + + /** + * Builds the signature of this node. + * + * @param node The method node + * + * @return The signature of the node + */ + public static ApexOperationSignature of(ASTMethod node) { + Visibility visibility = Visibility.get(node); + int code = code(visibility); + if (!POOL.containsKey(code)) { + POOL.put(code, new ApexOperationSignature(visibility)); + } + return POOL.get(code); + } + + +} diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexSignature.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexSignature.java new file mode 100644 index 000000000..d780e5d75 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/metrics/signature/ApexSignature.java @@ -0,0 +1,53 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.signature; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode; + +/** + * Base class for apex field or method signatures. + * + * @author Clément Fournier + */ +public abstract class ApexSignature { + + /** Visibility of the field or method. */ + public final Visibility visibility; + + + /** Create a signature using its visibility. */ + protected ApexSignature(Visibility visibility) { + this.visibility = visibility; + } + + + /** Visibility of a field or method. */ + public enum Visibility { + PRIVATE, PUBLIC, PROTECTED, GLOBAL; + + + /** + * Finds out the visibility of a method node. + * + * @param method The method node + * + * @return The visibility of the method + */ + public static Visibility get(ASTMethod method) { + ASTModifierNode modifierNode = method.getFirstChildOfType(ASTModifierNode.class); + if (modifierNode.isPublic()) { + return PUBLIC; + } else if (modifierNode.isPrivate()) { + return PRIVATE; + } else if (modifierNode.isProtected()) { + return PROTECTED; + } else { + return GLOBAL; + } + } + } + +} diff --git a/pmd-apex/src/main/resources/rulesets/apex/metrics.xml b/pmd-apex/src/main/resources/rulesets/apex/metrics.xml new file mode 100644 index 000000000..4d7468f92 --- /dev/null +++ b/pmd-apex/src/main/resources/rulesets/apex/metrics.xml @@ -0,0 +1,66 @@ + + + + + + These are rules which use the Metrics Framework to calculate metrics. + + + + + = 10. +Additionnally, classes with many methods of moderate complexity get reported as well once the total of their +methods' complexities reaches 40, even if none of the methods was directly reported. + +Reported methods should be broken down into several smaller methods. Reported classes should probably be broken down +into subcomponents. + ]]> + + 3 + + + + + + diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedNameTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedNameTest.java new file mode 100644 index 000000000..a85430f8f --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/ast/ApexQualifiedNameTest.java @@ -0,0 +1,86 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.List; + +import org.junit.Test; + +import apex.jorje.semantic.ast.compilation.Compilation; + +/** + * @author Clément Fournier + */ +public class ApexQualifiedNameTest { + + @Test + public void testClass() { + ApexNode root = ApexParserTestHelpers.parse("public class Foo {}"); + + ApexQualifiedName qname = ASTUserClass.class.cast(root).getQualifiedName(); + assertEquals("c__Foo", qname.toString()); + assertEquals(1, qname.getClasses().length); + assertNotNull(qname.getNameSpace()); + assertNull(qname.getOperation()); + } + + + @Test + public void testNestedClass() { + ApexNode root = ApexParserTestHelpers.parse("public class Foo { class Bar {}}"); + + ApexQualifiedName qname = root.getFirstDescendantOfType(ASTUserClass.class).getQualifiedName(); + assertEquals("c__Foo.Bar", qname.toString()); + assertEquals(2, qname.getClasses().length); + assertNotNull(qname.getNameSpace()); + assertNull(qname.getOperation()); + } + + + @Test + public void testSimpleMethod() { + ApexNode root = ApexParserTestHelpers.parse("public class Foo { String foo() {}}"); + ApexQualifiedName qname = root.getFirstDescendantOfType(ASTMethod.class).getQualifiedName(); + assertEquals("c__Foo#foo()", qname.toString()); + assertEquals(1, qname.getClasses().length); + assertNotNull(qname.getNameSpace()); + assertEquals("foo()", qname.getOperation()); + } + + + @Test + public void testMethodWithArguments() { + ApexNode root = ApexParserTestHelpers.parse("public class Foo { String foo(String h, Foo g) {}}"); + ApexQualifiedName qname = root.getFirstDescendantOfType(ASTMethod.class).getQualifiedName(); + assertEquals("c__Foo#foo(String,Foo)", qname.toString()); + assertEquals(1, qname.getClasses().length); + assertNotNull(qname.getNameSpace()); + assertEquals("foo(String,Foo)", qname.getOperation()); + } + + + @Test + public void testOverLoads() { + ApexNode root = ApexParserTestHelpers.parse("public class Foo { " + + "String foo(String h) {} " + + "String foo(int c) {}" + + "String foo(Foo c) {}}"); + + List methods = root.findDescendantsOfType(ASTMethod.class); + + for (ASTMethod m1 : methods) { + for (ASTMethod m2 : methods) { + if (m1 != m2) { + assertNotEquals(m1.getQualifiedName(), m2.getQualifiedName()); + } + } + } + } +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsHook.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsHook.java new file mode 100644 index 000000000..c376addaa --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsHook.java @@ -0,0 +1,25 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + + +/** + * Provides a hook into package-private methods of {@code apex.metrics}. + * + * @author Clément Fournier + */ +public class ApexMetricsHook { + + private ApexMetricsHook() { + + } + + + public static void reset() { + ApexMetrics.reset(); + } + + +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorTest.java new file mode 100644 index 000000000..b16f610dd --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexMetricsVisitorTest.java @@ -0,0 +1,69 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersionHandler; +import net.sourceforge.pmd.lang.apex.ApexLanguageModule; +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.ast.ApexParserTest; +import net.sourceforge.pmd.lang.apex.ast.ApexParserTestHelpers; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; +import net.sourceforge.pmd.lang.apex.metrics.signature.ApexOperationSigMask; + +import apex.jorje.semantic.ast.compilation.Compilation; + +/** + * @author Clément Fournier + */ +public class ApexMetricsVisitorTest extends ApexParserTest { + + @Test + public void testProjectMirrorNotNull() { + assertNotNull(ApexMetrics.getFacade().getProjectMirror()); + } + + + @Test + public void testOperationsAreThere() throws IOException { + ApexNode acu = parseAndVisitForString( + IOUtils.toString(ApexMetricsVisitorTest.class.getResourceAsStream("MetadataDeployController.cls"))); + + final ApexSignatureMatcher toplevel = ApexMetrics.getFacade().getProjectMirror(); + + final ApexOperationSigMask opMask = new ApexOperationSigMask(); + + // We could parse qnames from string but probably simpler to do that + acu.jjtAccept(new ApexParserVisitorAdapter() { + @Override + public Object visit(ASTMethod node, Object data) { + if (!node.getImage().matches("(||clone)")) { + assertTrue(toplevel.hasMatchingSig(node.getQualifiedName(), opMask)); + } + + return data; + } + }, null); + } + + + static ApexNode parseAndVisitForString(String source) { + LanguageVersionHandler languageVersionHandler = LanguageRegistry.getLanguage(ApexLanguageModule.NAME) + .getDefaultVersion().getLanguageVersionHandler(); + ApexNode acu = ApexParserTestHelpers.parse(source); + languageVersionHandler.getSymbolFacade().start(acu); + languageVersionHandler.getMetricsVisitorFacade().start(acu); + return acu; + } +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirrorTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirrorTest.java new file mode 100644 index 000000000..f7e65b4f9 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/ApexProjectMirrorTest.java @@ -0,0 +1,128 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics; + +import static net.sourceforge.pmd.lang.apex.metrics.ApexMetricsVisitorTest.parseAndVisitForString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClassOrInterface; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; +import net.sourceforge.pmd.lang.apex.metrics.impl.AbstractApexClassMetric; +import net.sourceforge.pmd.lang.apex.metrics.impl.AbstractApexOperationMetric; +import net.sourceforge.pmd.lang.metrics.Metric.Version; +import net.sourceforge.pmd.lang.metrics.MetricKey; +import net.sourceforge.pmd.lang.metrics.MetricKeyUtil; +import net.sourceforge.pmd.lang.metrics.MetricMemoizer; +import net.sourceforge.pmd.lang.metrics.MetricVersion; + +import apex.jorje.semantic.ast.compilation.Compilation; + +/** + * @author Clément Fournier + */ +public class ApexProjectMirrorTest { + + private static ApexNode acu; + private MetricKey> classMetricKey = MetricKeyUtil.of(new RandomClassMetric(), null); + private MetricKey opMetricKey = MetricKeyUtil.of(new RandomOperationMetric(), null); + + + static { + try { + acu = parseAndVisitForString( + IOUtils.toString(ApexMetricsVisitorTest.class.getResourceAsStream("MetadataDeployController.cls"))); + } catch (IOException ioe) { + // Should definitely not happen + } + } + + + @Test + public void memoizationTest() { + + + List expected = visitWith(acu, true); + List real = visitWith(acu, false); + + assertEquals(expected, real); + } + + + @Test + public void forceMemoizationTest() { + + List reference = visitWith(acu, true); + List real = visitWith(acu, true); + + assertEquals(reference.size(), real.size()); + + // we force recomputation so each result should be different + for (int i = 0; i < reference.size(); i++) { + assertNotEquals(reference.get(i), real.get(i)); + } + } + + + private List visitWith(ApexNode acu, final boolean force) { + final ApexProjectMemoizer toplevel = ApexMetrics.getFacade().getLanguageSpecificProjectMemoizer(); + + final List result = new ArrayList<>(); + + acu.jjtAccept(new ApexParserVisitorAdapter() { + @Override + public Object visit(ASTMethod node, Object data) { + MetricMemoizer op = toplevel.getOperationMemoizer(node.getQualifiedName()); + result.add((int) ApexMetricsComputer.INSTANCE.computeForOperation(opMetricKey, node, force, Version.STANDARD, op)); + return super.visit(node, data); + } + + + @Override + public Object visit(ASTUserClass node, Object data) { + MetricMemoizer> clazz = toplevel.getClassMemoizer(node.getQualifiedName()); + result.add((int) ApexMetricsComputer.INSTANCE.computeForType(classMetricKey, node, force, Version.STANDARD, clazz)); + return super.visit(node, data); + } + }, null); + + return result; + } + + + private class RandomOperationMetric extends AbstractApexOperationMetric { + + private Random random = new Random(); + + + @Override + public double computeFor(ASTMethod node, MetricVersion version) { + return random.nextInt(); + } + } + + private class RandomClassMetric extends AbstractApexClassMetric { + + private Random random = new Random(); + + + @Override + public double computeFor(ASTUserClassOrInterface node, MetricVersion version) { + return random.nextInt(); + } + } + +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexMetricTestRule.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexMetricTestRule.java new file mode 100644 index 000000000..5e8fc045f --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AbstractApexMetricTestRule.java @@ -0,0 +1,153 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import java.util.HashMap; +import java.util.Map; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetrics; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; +import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule; +import net.sourceforge.pmd.lang.metrics.Metric.Version; +import net.sourceforge.pmd.lang.metrics.MetricVersion; +import net.sourceforge.pmd.lang.metrics.ResultOption; +import net.sourceforge.pmd.lang.rule.properties.BooleanProperty; +import net.sourceforge.pmd.lang.rule.properties.DoubleProperty; +import net.sourceforge.pmd.lang.rule.properties.EnumeratedProperty; + +/** + * Abstract test rule for a metric. Tests of metrics use the standard framework for rule testing, using one dummy rule + * per metric. Default parameters can be overriden by overriding the protected methods of this class. + * + * @author Clément Fournier + */ +public abstract class AbstractApexMetricTestRule extends AbstractApexRule { + + private final EnumeratedProperty versionDescriptor = new EnumeratedProperty<>( + "metricVersion", "Choose a variant of the metric or the standard", + versionMappings(), Version.STANDARD, MetricVersion.class, 3.0f); + private final BooleanProperty reportClassesDescriptor = new BooleanProperty( + "reportClasses", "Add class violations to the report", isReportClasses(), 2.0f); + private final BooleanProperty reportMethodsDescriptor = new BooleanProperty( + "reportMethods", "Add method violations to the report", isReportMethods(), 3.0f); + private final DoubleProperty reportLevelDescriptor = new DoubleProperty( + "reportLevel", "Minimum value required to report", -1., Double.POSITIVE_INFINITY, defaultReportLevel(), 3.0f); + + private MetricVersion metricVersion; + private boolean reportClasses; + private boolean reportMethods; + private double reportLevel; + private ApexClassMetricKey classKey; + private ApexOperationMetricKey opKey; + + + public AbstractApexMetricTestRule() { + classKey = getClassKey(); + opKey = getOpKey(); + + definePropertyDescriptor(reportClassesDescriptor); + definePropertyDescriptor(reportMethodsDescriptor); + definePropertyDescriptor(reportLevelDescriptor); + definePropertyDescriptor(versionDescriptor); + } + + + /** + * Returns the class metric key to test, or null if we shouldn't test classes. + * + * @return The class metric key to test. + */ + protected abstract ApexClassMetricKey getClassKey(); + + + /** + * Returns the class metric key to test, or null if we shouldn't test classes. + * + * @return The class metric key to test. + */ + protected abstract ApexOperationMetricKey getOpKey(); + + + /** + * Sets the default for reportClasses descriptor. + * + * @return The default for reportClasses descriptor + */ + protected boolean isReportClasses() { + return true; + } + + + /** + * Sets the default for reportMethods descriptor. + * + * @return The default for reportMethods descriptor + */ + protected boolean isReportMethods() { + return true; + } + + + /** + * Mappings of labels to versions for use in the version property. + * + * @return A map of labels to versions + */ + protected Map versionMappings() { + Map mappings = new HashMap<>(); + mappings.put("standard", Version.STANDARD); + return mappings; + } + + + /** + * Default report level, which is 0. + * + * @return The default report level. + */ + protected double defaultReportLevel() { + return 0.; + } + + + @Override + public Object visit(ASTUserClass node, Object data) { + reportClasses = getProperty(reportClassesDescriptor); + reportMethods = getProperty(reportMethodsDescriptor); + reportLevel = getProperty(reportLevelDescriptor); + metricVersion = getProperty(versionDescriptor); + + + if (classKey != null && reportClasses && classKey.supports(node)) { + int classValue = (int) ApexMetrics.get(classKey, node, metricVersion); + + String valueReport = String.valueOf(classValue); + + if (opKey != null) { + int highest = (int) ApexMetrics.get(opKey, node, metricVersion, ResultOption.HIGHEST); + valueReport += " highest " + highest; + } + if (classValue >= reportLevel) { + addViolation(data, node, new String[] {node.getQualifiedName().toString(), valueReport}); + } + } + return super.visit(node, data); + } + + + @Override + public Object visit(ASTMethod node, Object data) { + if (opKey != null && reportMethods && opKey.supports(node)) { + int methodValue = (int) ApexMetrics.get(opKey, node, metricVersion); + if (methodValue >= reportLevel) { + addViolation(data, node, new String[] {node.getQualifiedName().toString(), "" + methodValue}); + } + } + return data; + } +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java new file mode 100644 index 000000000..b11423671 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/AllMetricsTest.java @@ -0,0 +1,35 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetricsHook; +import net.sourceforge.pmd.testframework.SimpleAggregatorTst; + +/** + * Executes the metrics testing rules. + * + * @author Clément Fournier + */ +public class AllMetricsTest extends SimpleAggregatorTst { + + + private static final String RULESET = "rulesets/apex/metrics_test.xml"; + + + @Override + protected Rule reinitializeRule(Rule rule) { + ApexMetricsHook.reset(); + return rule; + } + + + @Override + public void setUp() { + addRule(RULESET, "CycloTest"); + addRule(RULESET, "WmcTest"); + } + +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloTestRule.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloTestRule.java new file mode 100644 index 000000000..dc2f1d128 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/CycloTestRule.java @@ -0,0 +1,28 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; + +/** + * Tests standard cyclo. + * + * @author Clément Fournier + */ +public class CycloTestRule extends AbstractApexMetricTestRule { + + @Override + protected ApexClassMetricKey getClassKey() { + return null; + } + + + @Override + protected ApexOperationMetricKey getOpKey() { + return ApexOperationMetricKey.CYCLO; + } + +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcTestRule.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcTestRule.java new file mode 100644 index 000000000..a5d71e5be --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/metrics/impl/WmcTestRule.java @@ -0,0 +1,31 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.metrics.impl; + +import net.sourceforge.pmd.lang.apex.metrics.api.ApexClassMetricKey; +import net.sourceforge.pmd.lang.apex.metrics.api.ApexOperationMetricKey; + +/** + * @author Clément Fournier + */ +public class WmcTestRule extends AbstractApexMetricTestRule { + + @Override + protected boolean isReportMethods() { + return false; + } + + + @Override + protected ApexClassMetricKey getClassKey() { + return ApexClassMetricKey.WMC; + } + + + @Override + protected ApexOperationMetricKey getOpKey() { + return null; + } +} diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/metrics/MetricsRulesTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/metrics/MetricsRulesTest.java new file mode 100644 index 000000000..ec2fae809 --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/metrics/MetricsRulesTest.java @@ -0,0 +1,31 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.rule.metrics; + +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.lang.apex.metrics.ApexMetricsHook; +import net.sourceforge.pmd.testframework.SimpleAggregatorTst; + +/** + * @author Clément Fournier + */ +public class MetricsRulesTest extends SimpleAggregatorTst { + + + private static final String RULESET = "apex-metrics"; + + + @Override + protected Rule reinitializeRule(Rule rule) { + ApexMetricsHook.reset(); + return super.reinitializeRule(rule); + } + + + @Override + public void setUp() { + addRule(RULESET, "CyclomaticComplexity"); + } +} diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/MetadataDeployController.cls b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/MetadataDeployController.cls new file mode 100644 index 000000000..12af4169b --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/MetadataDeployController.cls @@ -0,0 +1,41 @@ +public with sharing class MetadataDeployController +{ + private class Foo { + } + + global String ZipData { get; set; } + + public MetadataService.AsyncResult AsyncResult {get; private set;} + + public String getPackageXml(String page) + { + return '' + + '' + + '' + + 'HelloWorld' + + 'ApexClass' + + '' + + '26.0' + + ''; + } + + public String getHelloWorldMetadata() + { + return '' + + '' + + '28.0' + + 'Active' + + ''; + } + + public String getHelloWorld() + { + return 'public class HelloWorld' + + '{' + + 'public static void helloWorld()' + + '{' + + 'System.debug(\' Hello World\');' + + '}' + + '}'; + } +} \ No newline at end of file diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CycloTest.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CycloTest.xml new file mode 100644 index 000000000..6b1771c72 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/CycloTest.xml @@ -0,0 +1,150 @@ + + + + + + + + + Complicated method - Standard + 2 + + 'c__Complicated#exception()' has value 4. + 'c__Complicated#example()' has value 18. + + + + + + + Empty methods should count 1 + false + 1 + + 'c__Foo#foo()' has value 1. + + + + + + + + + + + + #984 Cyclomatic complexity should treat constructors like methods + false + 1 + + 'c__Test#Test()' has value 4. + + + + + + 2 || y < 4) { + while (x++ < 10 && !(y++ < 0)); + } else if (a && b || x < 4) { + return; + } + } + } + ]]> + + + + Standard Cyclo should count boolean paths + 1 + false + + 'c__Foo#foo()' has value 8. + + + + + + Ternary expression counts 1 + boolean complexity + 1 + + 'c__Foo#bar()' has value 3. + + + + + + + diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/WmcTest.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/WmcTest.xml new file mode 100644 index 000000000..d4948fa2b --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/metrics/impl/xml/WmcTest.xml @@ -0,0 +1,95 @@ + + + + + + + + + Complicated class + 1 + + 'c__Complicated' has value 16. + + + + + + Empty classes count 0 + 1 + + 'c__Foo' has value 0. + + + + + + + + Abstract classes and enums are supported + 1 + + 'c__Foo' has value 2. + + + + + + + + Annotations and interfaces are not supported + 0 + + + + + + \ No newline at end of file diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/metrics/xml/CyclomaticComplexity.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/metrics/xml/CyclomaticComplexity.xml new file mode 100644 index 000000000..cb4f12a92 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/metrics/xml/CyclomaticComplexity.xml @@ -0,0 +1,252 @@ + + + + + + + + + Simple method + 1 + 1 + 2 + + The class 'Foo' has a total cyclomatic complexity of 1 (highest 1). + The method 'foo()' has a cyclomatic complexity of 1. + + + + + + + + testLessComplicatedThanReportLevel + 0 + + + + + + + Complicated method + 1 + + The method 'example()' has a cyclomatic complexity of 14. + + + + + + Constructor + 1 + 1 + + The constructor 'Foo()' has a cyclomatic complexity of 1. + + + + + + + + Test class report level + 14 + 999 + 1 + + + + + Test method report level + 999 + 14 + 1 + + + + + + + + + #984 Cyclomatic complexity should treat constructors like methods: 1 - reportMethods=true + 1 + 1 + + + + + #984 Cyclomatic complexity should treat constructors like methods: 2 -reportMethods=false + 999 + 0 + + + + + 2 || y < 4) { + while (x++ < 10 && !(y-- < 0)); + } else if (a && b || x < 4) { + return; + } + } + } + ]]> + + + + Standard Cyclo should count boolean paths + 2 + 1 + + The method 'foo()' has a cyclomatic complexity of 8. + + + + + + + + + + + Test many unreported methods + 1 + + The class 'Complicated' has a total cyclomatic complexity of 40 (highest 8). + + + + + \ No newline at end of file diff --git a/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml b/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml new file mode 100644 index 000000000..9864f7e3f --- /dev/null +++ b/pmd-apex/src/test/resources/rulesets/apex/metrics_test.xml @@ -0,0 +1,24 @@ + + + + + + Metrics testing ruleset. Each metric is tested through a dummy rule. + + + + + + + + + diff --git a/src/site/markdown/overview/changelog.md b/src/site/markdown/overview/changelog.md index 5e45ae798..394da3cc9 100644 --- a/src/site/markdown/overview/changelog.md +++ b/src/site/markdown/overview/changelog.md @@ -50,6 +50,9 @@ on the new metrics framework for object-oriented metrics. There are already a couple of metrics (e.g. ATFD, WMC, Cyclo, LoC) implemented. More metrics are planned. Based on those metrics, rules like "GodClass" detection can be implemented more easily. +The Metrics framework has been abstracted and is available in `pmd-core` for other languages. With this +PMD release, the metrics framework is supported for both Java and Apex. + #### Configuration Error Reporting For a long time reports have been notified of configuration errors on rules, but they have remained hidden. @@ -81,7 +84,6 @@ and include them to such reports. * The deprecated rule `UseSingleton` has been removed from the ruleset `java-design`. The rule has been renamed long time ago to `UseUtilityClass`. - #### Java Symbol Table A [bug in symbol table](https://github.com/pmd/pmd/pull/549/commits/0958621ca884a8002012fc7738308c8dfc24b97c) prevented @@ -90,8 +92,9 @@ rule, but other rules may now produce improved results as consequence of this fi #### Apex Parser Update -The Apex parser version was bumped, from `1.0-sfdc-187` to `1.0-sfdc-224`. This update let us take full advatange +The Apex parser version was bumped, from `1.0-sfdc-187` to `1.0-sfdc-224`. This update let us take full advantage of the latest improvements from Salesforce, but introduces some breaking changes: + * `BlockStatements` are now created for all control structures, even if no brace is used. We have therefore added a `hasCurlyBrace` method to differentiate between both scenarios. * New AST node types are available. In particular `CastExpression`, `ConstructorPreamble`, `IllegalStoreExpression`, @@ -100,7 +103,7 @@ of the latest improvements from Salesforce, but introduces some breaking changes * Some nodes have been removed. Such is the case of `TestNode`, `DottedExpression` and `NewNameValueObjectExpression` (replaced by `NewKeyValueObjectExpression`) -Al existing rules have been updated to reflect these changes. If you have custom rules, be sure to update them. +All existing rules have been updated to reflect these changes. If you have custom rules, be sure to update them. ### Fixed Issues @@ -161,5 +164,5 @@ Al existing rules have been updated to reflect these changes. If you have custom * [#530](https://github.com/pmd/pmd/pull/530): \[java] Fix issue #527: Lombok getter annotation on enum is not recognized correctly - [Clément Fournier](https://github.com/oowekyala) * [#535](https://github.com/pmd/pmd/pull/535): \[apex] Fix broken Apex visitor adapter - [Clément Fournier](https://github.com/oowekyala) * [#542](https://github.com/pmd/pmd/pull/542): \[java] Metrics abstraction - [Clément Fournier](https://github.com/oowekyala) +* [#545](https://github.com/pmd/pmd/pull/545): \[apex] Apex metrics framework - [Clément Fournier](https://github.com/oowekyala) * [#548](https://github.com/pmd/pmd/pull/548): \[java] Metrics documentation - [Clément Fournier](https://github.com/oowekyala) -