House static-analysis rules, published as two artifacts:
| artifact | tool | analyses |
|---|---|---|
io.github.joke.lint:pmd-rules |
PMD 7 | Java |
io.github.joke.lint:codenarc-rules |
CodeNarc 4 | Groovy, aimed at Spock specifications |
Both are implemented as Java rule classes and shipped as rulesets you reference from your build. They version independently and are consumed independently — there is no reason to take both.
Moving from
io.github.joke.pmd:rules? It is nowio.github.joke.lint:pmd-rules. The ruleset resource paths are unchanged, so only the dependency coordinate moves. The old coordinate carries a relocation POM, so Maven and Gradle will tell you the same thing.
dependencies {
pmd 'io.github.joke.lint:pmd-rules:<version>'
}
pmd {
ruleSets = ['rulesets/java/joke-strict.xml']
}That is the whole analysis this project runs on itself: PMD's bestpractices, codestyle, design,
errorprone, multithreading and performance categories, composed with the exclusions and
property overrides that make them workable, plus every rule this artifact defines. It needs PMD
7.26.0 or later — see PMD versions.
To keep your own composition and take only the rules from here, reference the convenience ruleset and add PMD's categories yourself:
pmd {
ruleSets = [
'category/java/bestpractices.xml', // PMD's own categories, if you want them
'rulesets/java/joke.xml', // every rule this artifact defines
]
}rulesets/java/joke.xml is a convenience selection of everything here. To pick rules individually,
reference them out of the catalogue instead:
pmd {
ruleSets = ['category/java/joke.xml/UseVarForLocalVariables']
}If you enable PMD's codestyle category alongside these rules, exclude TooManyStaticImports. It is
in direct opposition to UseStaticImports, which this artifact defines, and no configuration
satisfies both. rulesets/java/joke-strict.xml already excludes it.
ruleSets cannot subtract. Wanting joke-strict.xml minus one rule means writing your own
ruleset file that references it and excludes from it, and pointing ruleSetFiles at that:
<rule ref="rulesets/java/joke-strict.xml">
<exclude name="SomeRuleYouDisagreeWith"/>
</rule>dependencies {
codenarc 'io.github.joke.lint:codenarc-rules:<version>'
}
codenarc {
config = resources.text.fromString("ruleset { ruleset('rulesets/groovy/joke-strict.groovy') }")
}That is the whole analysis this project runs on its own Groovy: the curated selection of CodeNarc stock rules, plus every rule this artifact defines. It needs CodeNarc 4.0.0 or later — see CodeNarc versions.
Why the stub? Gradle cannot take a classpath ruleset. Its CodeNarcInvoker passes the ruleset to
CodeNarc's Ant task as "file:" + config, and CodeNarcActionParameters exposes no classpath
option — so unlike PMD's ruleSets, you cannot simply name rulesets/groovy/joke-strict.groovy.
CodeNarc's own loader does resolve nested ruleset(...) references from the classpath, so the
one-line stub above is the whole indirection. The stub is a pointer, not a policy: the composition
lives in the artifact, and you get its updates by bumping the dependency.
The outer ruleset { } is required. CodeNarc's RuleSetBuilder exposes only ruleset(Closure) at
the top level; ruleset(String) is a method on the closure's delegate, so a bare
ruleset('rulesets/...') fails at analysis time.
To take only the rules from here and keep your own composition, reference the convenience ruleset instead:
codenarc {
config = resources.text.fromString("ruleset { ruleset('rulesets/groovy/joke.groovy') }")
}rulesets/groovy/joke.groovy contains every rule this artifact defines and names no CodeNarc stock
rule, so it resolves identically under every supported CodeNarc.
The rules are compiled against CodeNarc 4.0.0 and run on any later 4.x.
CodeNarc's compatibility surface is two-dimensional in a way PMD's is not: the artifact coordinate itself encodes the Groovy line.
| line | coordinate | supported |
|---|---|---|
| Groovy 3 | org.codenarc:CodeNarc:3.x |
no |
| Groovy 4 | org.codenarc:CodeNarc:3.x-groovy-4.0 |
no |
| Groovy 5 | org.codenarc:CodeNarc:4.0.0 and later |
yes |
The rule classes touch only AbstractAstVisitorRule, AbstractAstVisitor, Violation,
SourceCode and org.codehaus.groovy.ast.*, so they would plausibly run on the older lines too —
but nothing tests that, and a plausible claim is not a supported one. The Groovy 4 line additionally
cannot carry the shipped composition at its own oldest release: joke-strict.groovy names
SpockMissingAssert, which CodeNarc only added in 3.3.0.
The published POM declares no dependencies. You supply CodeNarc yourself, on Gradle's codenarc
configuration, at a version you choose.
The rules are compiled against PMD 7.0.0 and run on any later PMD 7.x. Compiling against the
floor rather than the newest release is deliberate: rules built against an older API run on newer
PMD, whereas rules built against a newer API fail with NoSuchMethodError inside your analysis, at a
point where the stack trace blames PMD rather than this artifact. Every release runs its integration
tests against both the floor and the newest supported version.
The shipped resources do not all carry the same floor:
| resource | PMD floor | why |
|---|---|---|
| the rule classes | 7.0.0 | compiled against the floor API |
category/java/joke.xml |
7.0.0 | names only rules this artifact defines |
rulesets/java/joke.xml |
7.0.0 | references only category/java/joke.xml |
rulesets/java/joke-strict.xml |
7.26.0 | names PMD stock rules this artifact does not own |
The first three reference none of PMD's stock categories and exclude nothing from them, so they resolve identically under every PMD 7 version. Composing them with PMD's own categories is yours to do, which is what keeps your PMD version yours to choose.
rulesets/java/joke-strict.xml is the exception, and it is why the composition can ship at all. A
file naming stock rules is pinned to the PMD versions that still spell those rules the same way: a
renamed or removed rule is a hard ruleset-load failure, not a warning. Its floor is 7.26.0 rather
than 7.0.0 for a concrete reason — it excludes ImplicitFunctionalInterface, which PMD added after
7.0.0. That floor is the range this project supports and analyses itself with; no build check
asserts it.
On a PMD older than 7.26.0, use rulesets/java/joke.xml and compose the stock categories
yourself.
The published POM declares no dependencies. PMD comes from your pmd configuration at whatever
version you picked, and this artifact never overrides it. Every dependency the module declares sits
on a configuration that cannot reach the POM — compileOnly, annotationProcessor or a test
configuration — which is what keeps it empty.
The jar itself is Java 11 bytecode, so the JVM running PMD must be Java 11 or later. This says nothing about the source you analyse: analysing Java 8 code is a property of your PMD language version, not of this artifact.
Reports local variable declarations written with an explicit type where var would compile and
preserve the declared type. Repeating a type the compiler already knows adds no information and makes
the declaration harder to change.
Reported — single-variable local declarations, including basic and enhanced for loop variables and
try-with-resources:
String name = "joke"; // reported
for (int i = 0; i < 3; i++) { } // reported
for (String s : names) { } // reported
try (InputStream in = open()) { } // reportedNot reported, because var cannot express them:
int uninitialized; // var requires an initializer
String nothing = null; // null has no type to infer
int[] shorthand = {1, 2}; // var forbids the array-initializer form
Runnable task = () -> { }; // a lambda needs a target type
Supplier<String> make = String::new; // so does a method reference
int a = 1, b = 2; // var forbids multiple declaratorsFields and method, constructor, catch and lambda parameters are out of scope entirely.
The rule declares minimumLanguageVersion="10", so PMD skips it for source analysed at an earlier
language version rather than reporting violations you cannot act on.
Reports a static method that neither writes private static state, belongs to a utility class, nor
is a named constructor.
This is about what the modifier tells a reader, not about mocking — Mockito's inline mock maker and
Spock's SpyStatic both mock statics, so a mockability argument would simply be false. Left
unconstrained, static means any of five things: helper, factory, constant accessor, entry point,
class-state mutator. It therefore means nothing. Under this rule it means exactly one thing, and
seeing it tells you to go find the field.
static String format(int n) { return "" + n; } // reported: a helper, not state
static int get() { return count; } // reported: a read is not a write
static void register(String k, String v) { // reported: nothing can tell put from size
REGISTRY.put(k, v);
}
static void bump() { count++; } // not reported: writes private static state
static void reset() { count = 0; } // not reported: so does this
static Example of(int x) { return new Example(); } // not reported: a named constructor
public static void main(String[] args) { } // not reported: the JVM requires staticA write is an assignment, a compound assignment, an increment or a decrement to a private static
field. It follows that a private static final field can never justify a static method, because a
final field is only ever assigned in its initializer — a registry built on one must either become a
utility class or move onto an injected instance.
A utility class is exempt: one declaring no instance methods and no public or protected constructor.
A class declaring no constructor is judged on its implicit one, which takes the class's own access,
so a public class with no declared constructor is not a utility class — which agrees with PMD's
stock UseUtilityClass. An interface has no constructor at all, and an enum's implicit constructor
is always private.
A type carrying an annotation named UtilityClass is exempt without the structural test. Lombok's
@UtilityClass privatises the constructor and makes every member static during annotation
processing, so the source PMD reads declares instance-looking methods and no constructor at all,
which fails both halves of the structural test on a type that is a utility class once compiled. The
annotation is matched by simple name, so this adds no dependency on Lombok — a project without it
simply never matches the name.
A named constructor is exempt: a static method whose declared return type is its own declaring
type, or an interface that type directly declares it implements. A test double over such a factory
could only return what the constructor it wraps already returns, so there is nothing to intercept and
no seam is lost — it is not a helper hiding behind static, it is the constructor, named.
public class Cost implements Comparable<Cost> {
static Cost finite(int amount) { … } // not reported: returns its own type
static Comparable<Cost> comparable(int a) { … } // not reported: a directly declared interface
static Number total(int a) { … } // reported: a factory for something else
static Cost[] all() { … } // reported: an array is not the constructor
static void configure(int a) { … } // reported: void names no type
}A superclass return type, or an interface inherited transitively rather than declared here, is still
reported. The comparison is on simple names, with no type resolution, for the same reason
UseVisibleForTestingAnnotation matches names: resolution needs an auxclasspath consumers
frequently do not configure, and a misconfigured one would make the rule silently pass. The cost is
that a factory returning a same-named type from another package is exempted too — a missed report
review can catch, which beats silent under-reporting nobody can see.
main(String[]) and @BeforeAll/@AfterAll methods are exempt because the platform forces them to
be static and an unfixable violation is worse than a missed one. A @MethodSource provider and a
Spring static @Bean are not exempt — the annotation names the provider in a string, so nothing
can pick it out from any other static method. @SuppressWarnings("PMD.StaticMethodsModifyStaticState")
is the expected response there, not a sign that something has gone wrong.
Reports a method declared private, or declared protected without a marker stating why. The legal
visibilities are public, package-private, and a protected whose intent is declared.
A private method cannot be reached from a test, so it is only ever exercised through whatever
public method calls it, and it cannot be stubbed when that caller is the thing you meant to test.
No annotation changes that, so no marker excuses a private method.
protected is reachable but is usually the wrong seam: it widens the API to every subclass in every
consumer's codebase, where package-private widens it only to the package the test lives in. So
package-private is the default internal form and protected is a stated exception, permitted when
the method carries one of two markers:
| Marker | Meaning |
|---|---|
@ApiStatus.OverrideOnly |
an extension point implementors override |
@VisibleForTesting |
a visibility widened to create a test seam |
private boolean check() { return true; } // reported
protected boolean verify() { return true; } // reported: unmarked
boolean inspect() { return true; } // not reported: package-private
public boolean isValid() { return true; } // not reported: public API
private Example() { } // not reported: constructors are out of scope
@Override
protected void hook() { } // not reported: visibility is not chosen here
@ApiStatus.OverrideOnly
protected void extend() { } // not reported: a declared extension point
@VisibleForTesting
protected boolean seam() { return true; } // not reported: a declared test seam
@VisibleForTesting
private boolean hidden() { return true; } // reported: no marker makes private reachableThe markers are matched by simple name, so both @OverrideOnly imported directly and
@ApiStatus.OverrideOnly qualified through its outer type are recognised. The set is hardcoded
rather than exposed as a rule property: letting each project choose which annotations legitimise
protected reintroduces exactly the per-project drift the rule exists to prevent.
This does not soften the rule. The count of undeclared legal forms is still zero — an unmarked
protected is reported exactly as before. What a marker hands back is a choice between two
documented intents, not a choice about whether to declare one, and the rule's purpose of removing
discretion survives intact.
The markers are necessary because package-private is not always a compliant rewrite. A protected
member on a published abstract base whose subclasses live in other packages and other modules is
unreachable if narrowed, so the rule without them demanded a rewrite that does not compile.
Constructors are out of scope — you do not spy a constructor, and a private one is required by
StaticMethodsModifyStaticState's utility-class exception. @Override methods are exempt, because
Java forbids narrowing an inherited visibility.
For anything the markers do not cover, suppress it:
@SuppressWarnings("PMD.AvoidPrivateAndProtectedMethods")
protected void extensionPoint() { }Reporting protected only when no subclass actually uses it would be better, and is not possible:
PMD analyses one compilation unit at a time, rules are copied per thread, and every violation must
be attached to a live parsed node. "Does any subclass exist" is a whole-module question. The markers
exist so the question never has to be asked — a declaration is readable from a single file, which
makes the check correct even across a module boundary and into a consumer's subclass. The one case
provable in a single file — protected in a final class — is already covered by PMD's stock
AvoidProtectedMethodInFinalClassNotExtending, which stays correct where it fires: nothing can
override in a final class, and no out-of-package subclass can exist there for a seam to reach.
Reports a package-private method that does not carry @VisibleForTesting.
AvoidPrivateAndProtectedMethods makes package-private the canonical form for an internal method.
This rule makes the widened visibility read as a deliberate test seam rather than a forgotten
modifier — which is the only reason the wider visibility was acceptable in the first place.
boolean check() { return true; } // reported: an unmarked seam
@VisibleForTesting
boolean verify() { return true; } // not reported
public boolean isValid() { return true; } // not reported: not package-private
@Test
void reportsTheViolation() { } // not reported: a JUnit test methodThe annotation is matched by simple name, so any declaration works — JetBrains, Guava, AndroidX
and Elastic all ship a VisibleForTesting and all of them are markers. You do not need the one this
project happens to use, and there is no fully-qualified-name list to keep in sync. Matching the name
also avoids PMD's type resolution, which needs an auxclasspath that consumers frequently do not
configure and which would make the rule silently pass when misconfigured.
Only methods are in scope; fields, constructors and nested classes are not. @Override methods are
exempt, and so are JUnit 5 test and lifecycle methods, which are conventionally package-private and
for which the annotation would be nonsense.
Note that a package-private method is stubbable only from a test in the same package and the same classloader. That holds for a standard Gradle layout and fails under JPMS with a sealed module.
Reports a lambda whose body is a block. Logic belongs in something with a name — a lambda block body is anonymous by construction, so no test can call it, no caller can stub it, and its branches are reachable only through the pipeline that encloses it.
A method reference is not required. This is the part to read twice, because the intuitive reading would make the rule close to unusable: a lambda that closes over a local variable cannot become a method reference at all. Any non-block body satisfies the rule, so such a lambda stays a lambda and simply delegates:
items.forEach(item -> { // reported
validate(item, context);
save(item, context);
});
items.forEach(item -> process(item, context)); // fine — still a lambda, just not a block
items.forEach(this::save); // fine — no body at all
map(x -> { return x + 1; }); // reported: converts to an expression
map(x -> { save(x); }); // reported: so does this
map(x -> x + 1); // fine
Runnable task = () -> { }; // fine: nothing to extractAn empty block is exempt — () -> { } has nothing to extract, and a violation nobody can act on is
worse than one that is missed. A block containing only a comment is exempt for the same reason: a
comment is not a statement.
Logic inside an expression body is deliberately not reported:
map(x -> x > 0 ? positive(x) : negative(x)); // not reported — two branches
map(x -> x.getA().getB().stream().filter(…).count()); // not reported — a long chainBlock-versus-expression is a syntactic proxy for "logic hiding in an anonymous place". It is a good proxy and a cheap one to determine, and it is the start rather than the whole answer.
The rule declares minimumLanguageVersion="8", since lambdas do not exist before Java 8.
Extracting it produces a method that must be static — a static initializer calls it — and
StaticMethodsModifyStaticState then reports that method. No form satisfies both rules, so
@SuppressWarnings is available:
@SuppressWarnings("PMD.StaticMethodsModifyStaticState")
static String makeGreeting() { … }Usually there is a better fix: drop static from the field. The shape that produces these in
bulk is the static dispatch table, where every handler is forced static and every one is a violation:
// one suppression per handler, and another on every handler added
private static final Map<String, Handler> HANDLERS = Map.of(
"create", Example::handleCreate,
"delete", Example::handleDelete);
// no suppressions at all — the handlers are instance methods now
private final Map<String, Handler> handlers = Map.of(
"create", this::handleCreate,
"delete", this::handleDelete);Reports an anonymous class whose body is not empty. Its logic has no name: nothing can instantiate it, nothing can stub it. It is the less testable of the two anonymous forms — unlike a lambda it can declare several methods and carry its own fields.
return new Runnable() { // reported
@Override
public void run() { doTheWork(); }
};
return new Worker(); // fine — a named classThe two rules ship together because each is the other's bypass. A ban on lambda block bodies
alone is escaped in one edit by rewriting the lambda as an anonymous class, and every other rule here
waves that through: it is not static, its method is public and @Override, and it is not a lambda.
The escape would land you on the less testable construct.
Two exemptions, both because the alternative is a violation nobody can act on:
new TypeToken<List<String>>() { } // not reported: the empty body IS the mechanism
enum Op {
PLUS { int apply(int a, int b) { return a + b; } }, // not reported: an enum constant body
MINUS { int apply(int a, int b) { return a - b; } };
}PMD models an enum constant body as an anonymous class, so without that exemption the rule would report every strategy enum — which has no anonymous-free rewrite that keeps the enum. An anonymous class declared inside a method of an enum is still reported; only the constant's own body is exempt.
Reports a static member — method or field — reached through its declaring type. The import carries the owner; the code should not repeat it on every use.
If you enable
category/java/codestyle.xml, you must excludeTooManyStaticImports. It caps static imports at four by default, and this rule only ever adds them. No configuration satisfies both. This is the one rule here that fights a stock rule rather than composing with it.<rule ref="category/java/codestyle.xml"> <exclude name="TooManyStaticImports"/> </rule>
Error Prone's
BadImportneeds nothing from you. It rejects static imports of a handful of names this rule would otherwise demand,copyOfamong them, which would leave you with one report asking for the import and one refusing it.copyOfis excluded here for that reason — and because it is uninformative anyway — so the two tools agree without a suppression on either side.
The threshold is a floor, not a ceiling. The rule only ever says "import this". It never reports
an import as unnecessary and never stops you importing a shorter name by hand, so anything below the
floor is simply your call, and a report you disagree with is a @SuppressWarnings away.
Mockito.doReturn(true) // reported: 8 characters
Collections.unmodifiableList(xs) // reported: self-describing, so not excluded
Collectors.toList() // reported
AccessType.WRITE // reported: fields count too
Math.max(a, b) // not reported: 3 characters, under the floor
Optional.empty() // not reported: empty what?
Duration.ofSeconds(3) // not reported: of[A-Z] prefix
Registry.INSTANCE // not reported: instance of what?
List.copyOf(xs) // not reported: a copy of what, into what?
Example.class.getName() // not reported: a class literal cannot be importedShort names are left alone because the short static members of the JDK are overwhelmingly the
ambiguous ones — of, get, min, max, now, abs, sum.
Ambiguity is handled structurally, not by the exclusion list. If a file uses both
Arrays.toString and Objects.toString, neither is reported and you may import one, the other, or
neither. If it uses only one, the bare name is unambiguous in that file and the import line names
the owner. A name already bound in the file by a method, field, parameter or local variable is left
alone too, since the import would be shadowed.
The exclusion list is therefore only about uninformative names — factory-shaped members where the member name says what it produces but not of what:
exact: value values valueOf from empty create builder parse
now between copyOf getInstance newInstance INSTANCE
prefix: of… from… at a camelCase boundary, so ofSeconds is excluded and offer is not
Self-describing members such as unmodifiableList, toList and groupingBy are deliberately
absent, and are reported. The list is fixed — suppress at the site for your own factory methods.
Violations are reported once per member per file, not once per occurrence, because one import fixes them all. A first run over an existing codebase therefore reports roughly the number of import lines you need to add.
Reports a fully-qualified type name used in code where an import would let the simple name stand.
This does not overlap with PMD's stock UnnecessaryFullyQualifiedName, which sounds like it
covers the same ground and does not. That rule fires only when the simple name is already in
scope, and its fix is to drop the qualifier:
simple name already in scope → UnnecessaryFullyQualifiedName (stock) "drop the qualifier"
simple name not yet in scope → UseTypeImports (this artifact) "add an import"
The two partition the space, so enabling both never produces two reports for one name.
private java.util.List<String> names; // reported: add an import
private java.time.Duration timeout; // reported
private List<String> names; // not reported: imported
java.lang.String name = "joke"; // not reported: java.lang, the stock rule's job
private com.example.Helper helper; // not reported: same packageIf two types in one file want the same simple name, only one can be imported — the rule reports
neither and leaves the choice to you. A qualified nested type such as java.util.Map.Entry is
reported without prescribing which of the two valid fixes to apply. Reported once per name per file.
Note that when PMD cannot resolve a qualifier, both import rules stay silent rather than guess, so a clean run is not by itself proof of compliance.
They are designed to fire one at a time rather than all at once, so each violation has a single obvious fix. The full chain spans this ruleset and PMD's own:
items.forEach(item -> { validate(item); save(item); }); // AvoidLambdaBlockBodies: extract the body
items.forEach(item -> process(item)); // LambdaCanBeMethodReference (PMD stock)
items.forEach(this::process); // ↓ now the method itself
private static void process(…) // StaticMethodsModifyStaticState: drop static
private void process(…) // AvoidPrivateAndProtectedMethods: widen
void process(…) // UseVisibleForTestingAnnotation: annotate
@VisibleForTesting
void process(final Item item) { … } // cleanLambdaCanBeMethodReference is PMD's own rule, in category/java/codestyle.xml — this artifact
ships nothing that references it, but the two compose if you enable that category.
AvoidPrivateAndProtectedMethods deliberately skips static methods so that
StaticMethodsModifyStaticState reports them first, and AvoidLambdaBlockBodies stops at the block
body rather than also demanding a method reference. Expect several build runs when adopting these
rules on an existing codebase — each one surfaces the next step, and each step is mechanical.
Not every static method enters the chain. A named constructor — a static method returning its
own declaring type or a directly declared interface — is exempt, so it exits at the first step rather
than being reported by it and pushed toward an instance method it was never meant to become:
static Cost finite(int amount) { … } // exits here: a named constructor
private static void process(…) // continues down the chainUseStaticImports and UseTypeImports sit outside that chain: they report independently and each
violation is one import line.
Reports @Unroll on a specification class or a feature method.
Spock 2 unrolls every data-driven feature by default, so the annotation changes nothing. Left in place it reads as though it were switching a behaviour on, which sends a reader looking for the un-annotated features that supposedly behave differently.
class ExampleSpec extends Specification {
@Unroll // violation
def 'adds #a and #b'() {
expect:
a + b == sum
where:
a | b || sum
1 | 2 || 3
}
def 'adds #a and #b'() { // no violation: already unrolled
expect:
a + b == sum
where:
a | b || sum
1 | 2 || 3
}
}The annotation is matched on the name as written — both @Unroll and @spock.lang.Unroll — rather
than resolved to a type. CodeNarc analyses source without a compile classpath, so a rule that
resolved the annotation would report nothing whenever that classpath was incomplete. The cost is
that a deliberately misleading Unroll from another package is reported too, which is the cheaper
failure.
The rest of the house Spock conventions land one rule at a time.
./gradlew check
Runs the unit tests, the integration tests at each tool's compile floor, Spotless, PMD, CodeNarc, Error Prone with NullAway, and mutation testing at 100% mutation, coverage and test strength.
Groovy source is deliberately not formatted: Spotless's Groovy support is greclipse, which
reformats Spock's labelled-block layout badly enough to fight the specifications it would be tidying.
CodeNarc carries Groovy style instead.
Both artifacts analyse this repository with the artifacts this repository builds, through the same published resources a consumer references, resolved off the analysis classpath the same way:
| module | declares | task | analyses |
|---|---|---|---|
pmd-rules |
pmd project(':pmd-rules') |
pmdMain, pmdTest |
its own Java rule classes |
codenarc-rules |
pmd project(':pmd-rules') |
pmdMain |
its Java rule classes |
codenarc-rules |
codenarc project(':codenarc-rules') |
codenarcTest |
its own Spock specifications |
There is no ruleset file in this repository; both compositions under test are the shipped ones. A new rule therefore has to leave this repository clean as part of the change that adds it.
The CodeNarc rules are Spock-focused and this module's tests are Spock specifications, which is what makes that last row real dogfooding rather than a unit test wearing a costume.
The consequence is that a broken rule breaks the build that produces it, and the repair is to edit the rule that is currently failing. To build past it:
./gradlew check -x pmdMain -x pmdTest -x codenarcTest
Two signals, at the two ends of the range:
| signal | PMD version | comes from | ruleset | code analysed |
|---|---|---|---|---|
integrationTest |
7.0.0, the compile floor | the dependencies platform |
both shipped rulesets | synthetic fixtures |
pmdMain / pmdTest |
7.26.0 | the pmd-dist coordinate in the convention plugin |
joke-strict.xml |
this repository's real source |
A rule compiled against API absent from the floor fails integrationTest. A rule or ruleset broken
by a newer PMD fails pmdMain, against real source rather than fixtures. Versions between the two
ends are not exercised, which is a deliberate trade: a compile-floor break shows at the floor and an
API removal shows at the ceiling, so the interior was carrying no weight.
pmdMain therefore says nothing about the 7.0.0 floor on its own — integrationTest owns that — and
the pmd-dist version must stay at or above the floor rulesets/java/joke-strict.xml declares,
since pmdMain resolves that ruleset.
Adopting a newer PMD is one coordinate: raise net.sourceforge.pmd:pmd-dist in
buildSrc/src/main/groovy/conventions.gradle and run ./gradlew check. There is no version list to
extend and no guard to satisfy.
The pmd-test descriptors under pmd-rules/src/test/resources deliberately contain violating code, and
the examples in category/java/joke.xml do too. Both are invisible to pmdMain and pmdTest only
because they are XML. Do not move rule fixtures into .java files — the build would flag its own
test data, and excluding the fixture path to fix it would silently exclude whatever moved there
next.