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, org.codehaus.groovy.ast.* and one CodeNarc class marked internal, 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.
One internal CodeNarc class is used deliberately. org.codenarc.rule.junit.SpockUtil, whose
Javadoc says it is not intended for general use, supplies the specification gate, the feature-method
predicate and the Spock label vocabulary. The alternative was reimplementing them, which would make
this artifact's judgement drift silently from the stock Spock rules you compose it with; depending on
SpockUtil makes the same disagreement a loud failure instead.
The exposure is stated rather than mitigated away. A CodeNarc release that renames or removes
SpockUtil breaks these rules inside your analysis, not in this project's build. What the
dependency flow buys is that this repository fails first — Dependabot raises org.codenarc:CodeNarc
here, the build breaks, and the artifact is re-released. It does not protect you if you are
already running a CodeNarc newer than this artifact's latest release.
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.
Every rule below is gated on the class being a Spock specification, and reports nothing outside one. See The specification gate for what a consumer with a different base class sets.
Expect volume on a codebase that has not adopted strict mocking. The fixture rules fire on a
declaration here and there. The interaction rules judge a whole block, and
RequireStrictMockingTerminator alone reports once per then: block that does not end with 0 * _ —
which on a project that never adopted strict mocking is every then: block it has. A first run
producing hundreds of violations is the adoption cost of these conventions, not a broken ruleset.
Every rule is individually selectable and individually excludable. To take the artifact without one:
ruleset {
ruleset('rulesets/groovy/joke.groovy') {
exclude 'RequireStrictMockingTerminator'
}
}Fourteen rules against a checklist of fifteen. Each row names the convention and the rule that enforces it; the last row is the one left to review, and the reason it is left there.
| convention | rule |
|---|---|
No setup: or given: label — position already says it |
AvoidSetupAndGivenLabels |
Type name = Mock(), not def name = Mock(Type) or Type name = Mock(Type) |
DeclareMockWithExplicitType |
No @Shared or static double |
AvoidSharedOrStaticMock |
No interactions in the Mock() initialiser |
AvoidMockInitializerClosure |
Every interaction in then: |
InteractionsBelongInThenBlock |
Every mocked argument validated — no bare _ |
RequireValidatedInteractionArguments |
verifyAll for several properties of one argument |
UseVerifyAllForMultipleProperties |
Every then: ends with 0 * _ |
RequireStrictMockingTerminator |
Value assertions in expect:, not then: |
ValueAssertionsBelongInExpectBlock |
A spy's siblings are followed by 1 * subject._ |
RequireSpyEntryInteraction |
A spy interaction states its response — >>, or >> { callRealMethod() } |
RequireSpyInteractionResponse |
No cardinality on a stub — a stub is stubbed, a double that is counted is a Mock |
AvoidCardinalityOnStub |
SpyStatic is a setup statement, not a labelled one |
AvoidSpyStaticInLabelledBlock |
No @Unroll — Spock 2 unrolls by default |
AvoidUnrollAnnotation |
| One feature method per method under test, protected ones included | not enforced |
"One feature method per method under test" is deliberately not enforced, and no later version will enforce it. Deciding it needs the list of methods the subject declares. That list is in another file, and CodeNarc analyses source without a compile classpath, so a rule would have nothing to compare the specification against. Approximating it — matching feature-method names against method names, say — would report every specification whose feature names read as sentences, which is every specification this artifact ships. It stays a review convention.
Reports a setup: or given: statement label inside a feature method.
Spock treats unlabelled statements at the top of a feature method as the implicit setup block, so the
label states what the statement's position already says. Removed, the blank line before when:
carries the boundary.
class ExampleSpec extends Specification {
def 'charges the order total'() {
given: // violation
PaymentGateway gateway = Mock()
when:
service.checkout(order)
then:
1 * gateway.charge(49.99G)
0 * _
}
def 'refunds the order total'() {
PaymentGateway gateway = Mock() // no violation: position says it
when:
service.refund(order)
then:
1 * gateway.refund(49.99G)
0 * _
}
}when:, then:, expect:, where:, cleanup:, and:, filter: and combined: are untouched.
and: is a continuation with no semantic weight — CodeNarc's own label vocabulary excludes it for
that reason — and cleanup: and where: have no unlabelled equivalent, so reporting them would
demand a rewrite that does not exist.
A def setup() or def setupSpec() fixture method is not reported. The rule reports only inside a
method Spock would treat as a feature method, which is one carrying at least one statement label —
the same definition Spock's own SpecParser uses. That is what keeps the rule off the fixture
methods whose name it shares.
Reports a dynamically-typed declaration whose initialiser is a call to Mock, Stub or Spy.
The rule is about the declaration, not about Mock(Type). An inline Mock(Type) passed straight
to a constructor or a method is the documented way to supply a collaborator the specification never
refers to again, and is never reported — there is no variable whose type could have been written.
def service = new CheckoutService(Mock(PaymentGateway)) // no violation: declares no collaborator
service.register(Mock(Listener)) // no violation
def repository = Mock(CustomerRepository) // violation
CustomerRepository repository = Mock() // no violation
def service = Spy(OrderService, constructorArgs: [repo]) // violation
OrderService service = Spy(constructorArgs: [repo]) // no violationThe declared type is what a reader looks at to learn who the subject collaborates with. Moved into the initialiser it is still present but no longer in the position that answers the question, and the variable itself is untyped for every later line that uses it.
Fields are reported as well as local variables — a local is a declaration expression and a field is a field node with an initial expression, which are different nodes reached by different visits. Declaring collaborators as fields is the more common Spock form, so covering only locals would miss most of what the rule is for.
A typed declaration that repeats its type is reported too. Spock infers the double's type from the declared type of the variable, so the argument says nothing the left side does not.
TypeMirror mirror = Stub(TypeMirror) // violation
TypeMirror mirror = Stub() // no violation
Collection<String> items = Mock(List) // no violation: a different type
OrderService service = Spy(realService) // no violation: an instance, not a typeOnly the same type is reported. Collection<String> items = Mock(List) carries information the
declared type cannot, and removing the argument would change what is created. Types are compared by
name as written, generics ignored, and a qualified name matches its unqualified form — the import
decides which is written. Two differently qualified names are different types.
This rule reports more than it did. A consumer that already selects it can see new violations on upgrade, from declarations it was silent on before.
Reports a field that is @Shared or static and is initialised from Mock, Stub or Spy.
@Shared CustomerRepository repository = Mock() // violation
static CustomerRepository repository = Stub() // violation
CustomerRepository repository = Mock() // no violationA double that outlives the feature method is outside the scope in which Spock verifies interactions, so strict mocking cannot hold for it. Every double is created per feature method, where its interactions are checked.
One gap is documented rather than closed. @Shared Foo foo followed by foo = Mock() in
setupSpec has no initialiser to inspect. Catching it needs a write-tracking pass across methods,
and this artifact under-reports rather than infers. @Shared is matched by name as written, as
@Unroll is.
Reports a Mock, Stub or Spy call whose last argument is a closure.
CustomerRepository repository = Mock() { // violation
findById(_) >> customer
}
CustomerRepository repository = Mock() // no violation
...
then:
1 * repository.findById('cust-1') >> customer
0 * _An interaction declared in the initialiser sits away from the when: it answers and reads as
configuration rather than as verification. It also loses its cardinality: the initialiser form stubs
a return without asserting that the call happened, which is the half of the contract worth having.
Spy(constructorArgs: [repository]) passes a map rather than a closure and is not reported —
reporting it would leave no way to declare a spy over a real object.
def repository = Mock(Repo) { … } violates this rule and DeclareMockWithExplicitType, and is
reported by both. Each rule is independently selectable, so each has to be correct on its own;
suppressing one report because another rule also fires would leave a hole for whoever adopted only
one of the two.
Reports a Spock interaction anywhere in a feature method other than a then: block.
def 'charges the order total'() {
repository.findById('cust-1') >> customer // violation: reads as configuration
when:
def receipt = service.checkout(order)
then:
1 * gateway.charge(49.99G) >> receipt // no violation
0 * _
}An interaction declares what the subject must call. Placed above when: it reads as configuration
rather than verification and sits away from the action it answers; placed in expect: it mixes the
collaboration contract into the value assertions.
An interaction is recognised by its shape: a cardinality (1 * mock.foo(), (1..3) * mock.foo(),
_ * mock.foo()), a stubbed return (mock.foo() >> value, mock.foo() >>> [a, b]), the two combined,
or an interaction { … } block. Nothing asks whether the target is a mock, a stub, a spy or a real
object — CodeNarc analyses source without a compile classpath, and the shape is what distinguishes an
interaction in the first place. service.warmUp() and def total = 2 * price are not interactions.
and: continues the block it follows rather than starting one, matching CodeNarc's own label
vocabulary, which excludes and: because it carries no semantic weight. So an interaction in an
and: after then: is compliant, and the same and: after when: is not.
Reports a then: block whose last statement is not 0 * _.
then:
1 * gateway.charge(49.99G) >> receipt
0 * _ // no violation0 * _ asserts that no interaction other than the declared ones happened on any double. Without it a
specification silently tolerates an extra call, which is the regression strict mocking exists to
catch: an unplanned collaborator call should break a test until someone declares it on purpose.
The terminator is required whether or not the feature method declares a Mock, Stub or Spy.
Requiring it only when a double is in scope was rejected on the failing case rather than the common
one: a specification with no collaborators today acquires one the moment the subject grows a
dependency, and a conditional rule goes quiet exactly then — the then: block that was compliant
yesterday is unterminated and nothing reports it.
So a then: block containing nothing but the terminator is the intended shape, not an artefact. This
repository's own AvoidUnrollAnnotationRuleSpec is the worked example:
def 'the name and priority are settable, as CodeNarc ruleset configuration requires'() {
when:
rule.name = 'Renamed'
rule.priority = 3
then:
0 * _
expect:
rule.name == 'Renamed'
rule.priority == 3
}A then: and every and: following it are one run, terminated once at the end rather than once per
and: — a terminator per and: would assert "nothing else happened" in the middle of a list still
being declared. Only then: is asked for one: a feature method with no collaborators and a standalone
expect: is a documented shape, and demanding a then: block there would demand a block with nothing
to verify.
Reports a boolean expression in a then: block.
then:
1 * gateway.charge(49.99G) >> receipt
receipt.total == 49.99G // violation
0 * _
expect:
receipt.total == 49.99G // no violationthen: states who the subject called; expect: states what it returned. A then: block full of ==
buries the interaction contract in the middle of value checks, and the contract is the half a reader
most needs to find.
An interaction, a thrown(…) or notThrown(…) call, and a def error = thrown(…) capture all stay
where they are. The exception's message does not — error.message == '…' is a value assertion like
any other.
Known gap: a bare truthiness check is not reported. receipt.valid in a then: block stays
silent, while receipt.isValid() is reported. What counts as a value assertion is CodeNarc's own
boolean-expression test, which recognises comparison operators and a set of method-name patterns. The
alternative — report anything that is not an interaction and not thrown — would report an ordinary
helper call in a then: block, a false positive on code that is not wrong. Sharing CodeNarc's
judgement also means this rule and stock SpockMissingAssert never disagree about the same line. The
rule under-reports rather than guesses, and widening the definition later is a change with its own
evidence.
Reports an interaction argument that places no constraint on the value passed.
1 * repository.save(_) // violation
1 * repository.save(_ as Customer) // violation
1 * repository.save(*_) // violation
1 * repository.save({ true }) // violation
1 * repository.save(expectedCustomer) // no violation
1 * repository.save({ it.id == 'cust-1' }) // no violationA mocked interaction is a contract about what the subject passes its collaborator. An argument that matches anything lets a wrong value through and leaves the contract asserting only that a call happened, which is the weaker half.
_ as Type is reported too. It carries more information than a bare _, which makes it the closest
call in the set, but it still accepts every instance of that type and the rule's subject is whether
the argument was the right one.
For the closure form the parameter list is irrelevant, so only the body is read: { true },
{ _ -> true } and { it -> true } differ in a parameter the body never uses. A body of one
statement whose expression is a constant truthy under Groovy truth is reported, so { 1 } is a
violation and { 0 } is not.
Only argument positions are examined. _ in target position is required by this rule's own
family — 0 * _ by RequireStrictMockingTerminator — so 0 * _, 1 * service._ and
1 * repository._(expectedCustomer) are all silent.
The rule applies to every interaction form on every kind of double, and reports wherever the
interaction appears — including a field initialiser and a Mock() { … } closure — so an interaction
moved out of a then: block to escape a sibling rule is still checked here.
Known gap: a closure that is truthy without being a bare constant is not reported. { it } is
truthy for anything non-null and non-empty and constrains nothing, and { return true } is the same
closure written with a statement rather than an expression. Neither is reported: detecting the first
would begin a general truthiness analysis this rule declines to start, and the second is the same
check applied to a body shape the rule does not read. Both under-report rather than guess.
Reports an interaction's constraint closure that asserts more than one property without verifyAll.
1 * repository.save({ it.name == 'Ada' && it.region == 'eu' }) // violation: a chain
1 * repository.save({ it.name == 'Ada'; it.region == 'eu' }) // violation: two conditions
1 * auditLog.record({ it.action == 'CHECKOUT' }) // no violation: one property
1 * repository.save({ Customer c -> // no violation: verifyAll
verifyAll(c) {
name == 'Ada'
region == 'eu'
}
})verifyAll evaluates every condition and reports all failures at once. A chain stops at the first, so
a customer wrong in two fields reports one — and the second failure only surfaces after the first is
fixed and the specification is run again. That is the diagnostic a reader needs least: one more round
trip per wrong field.
A single property is exempt, and this is not an oversight. The convention says the inline boolean
closure is enough for one property, and a rule pushing verifyAll onto one condition would make the
common case wordier for nothing — there is only one failure to report either way. A disjunction is one
condition too: { it.region == 'eu' || it.region == 'uk' } is a single question about a single
property, not two assertions that could each fail.
with, verifyAll and verifyEach all satisfy the rule — CodeNarc's own list of methods carrying
implicit assertions — and a closure delegating to any of them is silent however many conditions it
holds. No carve-out implements that: a condition is recognised by CodeNarc's boolean-expression test,
and a call by any of those three names is not a boolean expression under it, so a wrapped closure
holds no conditions of its own and never reaches the threshold.
The rule examines interaction constraint closures only, wherever the interaction stands — including a
stubbed return, repository.findById({ it.id == 'x' && it.active }) >> customer.
expect: blocks are out of scope. The convention names verifyAll as the right tool for several
properties of a returned value without making it a requirement, and the distinction it rests on is not
available to static analysis: several assertions about one value and assertions about several
different values have the same shape unless the rule knows what the expressions denote. A rule
reporting every multi-assertion expect: block would report almost every specification this artifact
ships, including the ones testing this rule.
Without 1 * spy._, the 0 * _ terminator fails on the very call the feature method exists to
make. A spy's own methods count as interactions under strict mocking, and the method under test is
itself invoked once — by when:. That entry call is an interaction on the spy like any other, so
unless something accounts for it, 0 * _ reports it as the undeclared interaction it technically is.
The specification fails on its own subject.
This is the only rule in this artifact whose fix is to add a line. Every other one reports something present that should be moved or removed, and can be acted on without reading this section.
OrderService service = Spy(constructorArgs: [repository])
when:
service.placeOrder(order)
then:
1 * service.validate(order) // a sibling, stubbed on the spy
1 * repository.persist(order)
1 * service._ // required: accounts for placeOrder()
0 * _Reported when a then: block declares one or more specific interactions on a spy and no
1 * spy._. A block that constrains nothing on the spy needs no entry interaction — nothing in it is
about to fail the terminator — so the rule stays silent there rather than putting a line into feature
methods where it asserts nothing. 1 * spy._ is itself an interaction on the spy and does not count
as the specific interaction that triggers the requirement; counting the remedy as its own trigger
would satisfy the rule in exactly the blocks that never needed it.
The entry interaction must come last, and this is the only position check in the family. Spock
matches a declared interaction against a call in declaration order, and 1 * spy._ matches every
method on the spy:
then:
1 * service._ // violation: declared first
1 * service.validate(order) // ...so this never matches, and fails its count
0 * _Declared first, the entry interaction absorbs the sibling calls the specific interactions were written
to verify, and those then fail their own counts. The specification still runs and still fails — just
somewhere other than where the mistake is. Ordering earns the exception the other rules do not get
because the wrong order is silently wrong. Combined with RequireStrictMockingTerminator, the tail
of a spy then: block is fixed: the specific interactions, then 1 * spy._, then 0 * _.
A spy is recognised from a declaration whose initialiser is a Spy call — Spy(Type),
Spy(constructorArgs: [ … ]) and Spy(realInstance) alike — as a local in the feature method or as a
field on the specification. A spy arriving from anywhere else is invisible and the rule stays
silent. A subject handed over by a helper method or a base class cannot be recognised without
resolution, and a rule that guessed would demand 1 * x._ for a variable that is not a spy — a
violation whose only fix is to add a line that breaks the specification. Under-reporting is the safe
direction here in particular.
Reports a counted interaction on a spy that states no response.
1 * service.validate(order) // violation: calls the real method
1 * service.validate(order) >> true // no violation
1 * service.validate(order) >> { callRealMethod() } // no violation: the call-through, said aloud
1 * service._ // no violation: the entry interaction
0 * service.audit(order) // no violation: never calledWithout a response, an interaction on a spy calls the real method. That is rarely what
1 * service.validate(order) reads as, so the call-through is stated.
Exactly spy._ is exempt, and nothing wider. It is the short, deliberate way to allow the spy's
one self-call from when:, and RequireSpyEntryInteraction requires it. 1 * service._(order) is a
filter over every method's argument, and a pattern-matched method name selects methods nobody named;
either can call real code the author never listed, so both are reported. The cardinality on spy._ is
not read — the entry rule does not read it either, and the two must not disagree about 2 * service._.
A spy is recognised from its declaration, as RequireSpyEntryInteraction does it. A Mock returns a
default and a Stub an empty value, so neither is in scope, and a spy from a helper method, base class
or parameter is invisible. Interactions inside an interaction { } closure are not visited.
Reports an interaction on a stub that carries a cardinality other than _.
1 * repository.find(id) // violation
1 * repository.find(id) >> customer // violation: the response does not remove the count
0 * repository.find(id) // violation: Spock treats it as required too
(1..3) * repository.find(id) // violation
repository.find(id) >> customer // no violation
_ * repository.find(id) >> customer // no violation: not a required interaction
0 * _ // no violation: names no stubA stub is not verified, and Spock refuses a required interaction on one with InvalidSpecException: Stub '…' matches the following required interaction. An interaction is required unless its
cardinality is unbounded, so the only cardinality a stub accepts is _ *. The rule moves a failure the
specification would have at run time to analysis time, and to the line that causes it. A double whose
calls are counted is a Mock.
The forms above were established against Spock 2.4 rather than inferred:
written, against a default Stub() |
Spock |
|---|---|
1 * s.foo(), (1..3) * s.foo(), (0..2) * s.foo(), 0 * s.foo() |
throws |
1 * s.foo() >> x |
throws |
_ * s.foo(), s.foo() >> x |
passes |
0 * _ beside a stub |
passes |
Stub(verified: true), 1 * s.foo() |
passes |
A stub declared with a verified argument is not reported. Stub(verified: true) is allowed a
required interaction. The value is not read, because it may be an expression CodeNarc cannot resolve,
so any Stub call that passes verified is left alone.
A stub is recognised from its declaration, as the spy rules recognise a spy: a Stub(...) call
initialising a local in the feature method or a field on the specification. A stub arriving from a
helper method, a base class or a parameter is invisible and the rule stays silent. A Mock and a
Spy are verified and accept a cardinality, so neither is in scope. GroovyStub is not
recognised: the factory set this artifact reads does not include it, and teaching it to would change
DeclareMockWithExplicitType and AvoidSharedOrStaticMock in the same stroke. Interactions inside an
interaction { } closure are not visited.
Reports a call to SpyStatic under any Spock statement label.
def 'applies the configured regional tax rate'() {
SpyStatic(PricingRules) // no violation: unlabelled setup
def service = new InvoiceService()
when:
def total = service.totalWithTax(100G, 'eu')
then:
SpyStatic(PricingRules) // violation: not a verification
1 * PricingRules.taxRate('eu') >> 0.20G
0 * _
}SpyStatic enables static mocking for the whole feature method. It belongs with the other unlabelled
setup statements at the top, where a reader looks for what the method arranges. Placed in then: it
sits among interaction verifications and reads as one, which it is not — it declares nothing about
what was called.
The call is matched on the name as written, with no attempt to resolve it, exactly as
AvoidUnrollAnnotation matches @Unroll. CodeNarc analyses source without a compile classpath, so
name-matching is the only mechanism available, and the specification gate is what keeps the bare name
from matching elsewhere. A name that merely ends in SpyStatic is not matched.
This rule ships with no evidence beyond its own fixtures. SpyStatic is real API in the Spock this
artifact builds against — spock.mock.MockingApi declares it, SpecInternals.SpyStaticImpl implements
it, and Spock's own Identifiers lists it next to Mock, Stub and Spy — but no specification in
this repository calls it, so the rule has never fired on real code here. That is why it carries
priority 3 where the other thirteen carry 2.
Mock() { findById(_) >> row } violates AvoidMockInitializerClosure and
RequireValidatedInteractionArguments, and both fire. An interaction in the setup region passing _
violates InteractionsBelongInThenBlock and RequireValidatedInteractionArguments, and both fire. A
then: block full of == with no terminator is reported by ValueAssertionsBelongInExpectBlock once
per assertion and by RequireStrictMockingTerminator once for the block.
One pair cannot overlap, and that is by construction rather than by suppression:
RequireValidatedInteractionArguments reports a constraint closure whose body is a single truthy
constant, and UseVerifyAllForMultipleProperties requires more than one condition. No closure
satisfies both predicates, so { true } is reported once and only by the first.
Suppressing the second report because the first already condemned the line was considered and rejected. Every rule here is individually selectable, so a consumer may have adopted only one of the pair — and a rule that defers to a sibling would leave that consumer's ruleset silently missing the violation it selected a rule to catch. Two reports on one line is the lesser cost, and one edit removes both.
Every rule in this artifact reports only inside a class it recognises as a Spock specification, and each exposes the two properties CodeNarc's own Spock rules expose:
| property | default | matched against |
|---|---|---|
specificationSuperclassNames |
*Specification |
the superclass name as written |
specificationClassNames |
unset | the class name |
The rules match identifiers — Mock, Stub, Spy, and Spock's block labels — that are legal
elsewhere in Groovy, and DeclareMockWithExplicitType is the one for which a collision outside a
specification is realistic rather than theoretical. The gate is what keeps them off production code.
If your specifications extend a base class of your own, name it:
ruleset {
ruleset('rulesets/groovy/joke.groovy') {
'DeclareMockWithExplicitType' {
specificationSuperclassNames = '*Specification,*IntegrationBase'
}
}
}Both properties accept a comma-separated list and the * and ? wildcards. A specification whose
superclass matches neither is not analysed by any rule here.
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.
That is the whole checklist. Eleven rules cover every Spock convention this project holds that static analysis can decide; the twelfth is named in Coverage of the Spock conventions and stays a review convention.
./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.