• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 196

16 Oct 2025 08:33AM UTC coverage: 78.642% (-0.02%) from 78.661%
196

push

github

web-flow
chore: fix dogfood issues from new rules (#6056)

18180 of 23973 branches covered (75.84%)

Branch coverage included in aggregate %.

2 of 27 new or added lines in 14 files covered. (7.41%)

2 existing lines in 1 file now uncovered.

39693 of 49617 relevant lines covered (80.0%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.69
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/ModifierOrderRule.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.java.rule.codestyle;
6

7
import java.util.List;
8

9
import org.checkerframework.checker.nullness.qual.Nullable;
10

11
import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken;
12
import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
13
import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
14
import net.sourceforge.pmd.lang.java.ast.ASTLambdaParameter;
15
import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration;
16
import net.sourceforge.pmd.lang.java.ast.ASTModifierList;
17
import net.sourceforge.pmd.lang.java.ast.ASTType;
18
import net.sourceforge.pmd.lang.java.ast.ASTVoidType;
19
import net.sourceforge.pmd.lang.java.ast.JModifier;
20
import net.sourceforge.pmd.lang.java.ast.JavaNode;
21
import net.sourceforge.pmd.lang.java.ast.JavaTokenKinds;
22
import net.sourceforge.pmd.lang.java.ast.internal.PrettyPrintingUtil;
23
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
24
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
25
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
26
import net.sourceforge.pmd.properties.PropertyDescriptor;
27
import net.sourceforge.pmd.properties.PropertyFactory;
28
import net.sourceforge.pmd.reporting.RuleContext;
29
import net.sourceforge.pmd.util.AssertionUtil;
30
import net.sourceforge.pmd.util.OptionalBool;
31

32
/**
33
 * @since 7.17.0
34
 */
35
public class ModifierOrderRule extends AbstractJavaRulechainRule {
36

37
    private static final String MSG_KEYWORD_ORDER =
38
        "Missorted modifiers `{0} {1}`.";
39

40
    private static final String MSG_ANNOTATIONS_SHOULD_BE_BEFORE_MODS =
41
        "Missorted modifiers `{0} {1}`. Annotations should be placed before modifiers.";
42

43
    private static final String MSG_TYPE_ANNOT_SHOULD_BE_BEFORE_TYPE =
44
        "Missorted modifiers `{0} {1}`. Type annotations should be placed before the type they qualify.";
45

46
    private static final PropertyDescriptor<TypeAnnotationPosition> TYPE_ANNOT_POLICY
1✔
47
        = PropertyFactory.enumProperty("typeAnnotations", TypeAnnotationPosition.class, TypeAnnotationPosition::label)
1✔
48
                         .desc("Whether type annotations should be placed next to the type they qualify and not before modifiers.")
1✔
49
                         .defaultValue(TypeAnnotationPosition.ANYWHERE)
1✔
50
                         .build();
1✔
51

52
    public enum TypeAnnotationPosition {
1✔
53
        ON_TYPE,
1✔
54
        ON_DECL,
1✔
55
        ANYWHERE;
1✔
56

57
        String label() {
58
            switch (this) {
1!
59
            case ON_TYPE:
60
                return "ontype";
1✔
61
            case ON_DECL:
62
                return "ondecl";
1✔
63
            case ANYWHERE:
64
                return "anywhere";
1✔
65
            }
NEW
66
            throw AssertionUtil.shouldNotReachHere("exhaustive switch");
×
67
        }
68
    }
69

70
    private TypeAnnotationPosition typeAnnotPosition;
71

72
    public ModifierOrderRule() {
73
        super(ASTModifierList.class);
1✔
74
        definePropertyDescriptor(TYPE_ANNOT_POLICY);
1✔
75
    }
1✔
76

77
    @Override
78
    public void start(RuleContext ctx) {
79
        this.typeAnnotPosition = getProperty(TYPE_ANNOT_POLICY);
1✔
80
    }
1✔
81

82
    /** Wrapper around a mod to do "double dispatch". */
83
    abstract static class LastModSeen {
1✔
84
        abstract boolean checkNextKeyword(KwMod next, RuleContext ctx);
85

86
        abstract boolean checkNextAnnot(AnnotMod next, RuleContext ctx);
87

88
        @Override
89
        public abstract String toString();
90
    }
91

92
    class KwMod extends LastModSeen {
93
        private final JModifier mod;
94
        private final JavaccToken token;
95
        private final JavaNode reportNode;
96

97
        KwMod(JModifier mod, JavaccToken token, JavaNode reportNode) {
1✔
98
            this.mod = mod;
1✔
99
            this.token = token;
1✔
100
            this.reportNode = reportNode;
1✔
101
        }
1✔
102

103
        @Override
104
        boolean checkNextKeyword(KwMod next, RuleContext ctx) {
105
            if (mod.compareTo(next.mod) > 0) {
1✔
106
                ctx.addViolationWithPosition(reportNode, token, MSG_KEYWORD_ORDER, this, next);
1✔
107
                return true;
1✔
108
            }
109
            return false;
1✔
110
        }
111

112
        @Override
113
        boolean checkNextAnnot(AnnotMod next, RuleContext ctx) {
114
            // keyword before annot
115
            if (next.isTypeAnnot != OptionalBool.NO && typeAnnotPosition != TypeAnnotationPosition.ON_DECL) {
1✔
116
                return false;
1✔
117
            }
118
            ctx.addViolationWithPosition(reportNode, token, MSG_ANNOTATIONS_SHOULD_BE_BEFORE_MODS, this, next);
1✔
119
            return true;
1✔
120

121
        }
122

123
        @Override
124
        public String toString() {
125
            return mod.getToken();
1✔
126
        }
127
    }
128

129
    class AnnotMod extends ModifierOrderRule.LastModSeen {
130
        private final @Nullable LastModSeen previous;
131
        private final ASTAnnotation annot;
132
        private final OptionalBool isTypeAnnot;
133

134
        AnnotMod(@Nullable LastModSeen previous, ASTAnnotation annot, boolean contextsAcceptsTypeAnnot) {
1✔
135
            this.previous = previous;
1✔
136
            this.annot = annot;
1✔
137
            this.isTypeAnnot = !contextsAcceptsTypeAnnot ? OptionalBool.NO : isTypeAnnotation(annot);
1✔
138
        }
1✔
139

140

141
        @Override
142
        boolean checkNextKeyword(KwMod next, RuleContext ctx) {
143
            if (isTypeAnnot.isTrue() && typeAnnotPosition == TypeAnnotationPosition.ON_TYPE) {
1✔
144
                ctx.addViolationWithMessage(annot, MSG_TYPE_ANNOT_SHOULD_BE_BEFORE_TYPE, this, next);
1✔
145
                return true;
1✔
146
            }
147

148
            if (previous instanceof KwMod) {
1✔
149
                // annotation sandwiched between keywords
150
                if (isTypeAnnot.isTrue() && typeAnnotPosition != TypeAnnotationPosition.ON_DECL) {
1!
151
                    ctx.addViolationWithMessage(annot, MSG_TYPE_ANNOT_SHOULD_BE_BEFORE_TYPE, this, next);
1✔
152
                } else {
153
                    ctx.addViolationWithMessage(annot, MSG_ANNOTATIONS_SHOULD_BE_BEFORE_MODS, previous, this);
×
154
                }
155
                return true;
1✔
156
            }
157

158
            return false;
1✔
159
        }
160

161
        @Override
162
        boolean checkNextAnnot(AnnotMod next, RuleContext ctx) {
163
            // todo we could sort annotations (alphabetically or by length)
164
            return false;
1✔
165
        }
166

167
        @Override
168
        public String toString() {
169
            return PrettyPrintingUtil.prettyPrintAnnot(annot);
1✔
170
        }
171
    }
172

173
    @Override
174
    public Object visit(ASTModifierList modList, Object data) {
175
        RuleContext ctx = asCtx(data);
1✔
176
        boolean acceptsTypeAnnot = contextCanHaveTypeAnnots(modList);
1✔
177
        ModifierOrderEvents eventHandler = new ModifierOrderEvents() {
1✔
178

179
            private @Nullable LastModSeen lastModSeen;
180

181

182
            @Override
183
            public boolean recordAnnotation(ASTAnnotation annot) {
184
                AnnotMod annotMod = new AnnotMod(lastModSeen, annot, acceptsTypeAnnot);
1✔
185
                if (lastModSeen != null) {
1✔
186
                    if (lastModSeen.checkNextAnnot(annotMod, ctx)) {
1✔
187
                        return true;
1✔
188
                    }
189
                }
190
                lastModSeen = annotMod;
1✔
191
                return false;
1✔
192
            }
193

194
            @Override
195
            public boolean recordModifier(JModifier mod, JavaccToken token) {
196
                KwMod kwMod = new KwMod(mod, token, modList);
1✔
197
                if (lastModSeen != null) {
1✔
198
                    if (lastModSeen.checkNextKeyword(kwMod, ctx)) {
1✔
199
                        return true;
1✔
200
                    }
201
                }
202
                lastModSeen = kwMod;
1✔
203
                return false;
1✔
204
            }
205
        };
206

207
        readModifierList(modList, eventHandler);
1✔
208
        return null;
1✔
209
    }
210

211
    private static boolean contextCanHaveTypeAnnots(ASTModifierList modList) {
212
        ASTType followingType = getFollowingType(modList);
1✔
213
        return followingType != null && !(followingType instanceof ASTVoidType)
1✔
214
            || isFollowedByVarKeyword(modList)
1✔
215
            || modList.getParent() instanceof ASTConstructorDeclaration;
1✔
216
    }
217

218
    private static OptionalBool isTypeAnnotation(ASTAnnotation node) {
219
        JTypeDeclSymbol sym = node.getTypeNode().getTypeMirror().getSymbol();
1✔
220
        if (sym instanceof JClassSymbol) {
1!
221
            return ((JClassSymbol) sym).mayBeTypeAnnotation(node.getLanguageVersion());
1✔
222
        }
223
        return OptionalBool.UNKNOWN;
×
224
    }
225

226
    private static @Nullable ASTType getFollowingType(ASTModifierList node) {
227
        JavaNode nextSibling = node.getNextSibling();
1✔
228
        if (nextSibling instanceof ASTType) {
1✔
229
            return (ASTType) nextSibling;
1✔
230
        }
231
        return null;
1✔
232
    }
233

234
    private static boolean isFollowedByVarKeyword(ASTModifierList node) {
235
        JavaNode parent = node.getParent();
1✔
236
        if (parent instanceof ASTLambdaParameter) {
1✔
237
            return ((ASTLambdaParameter) parent).hasVarKeyword();
1✔
238
        } else if (parent instanceof ASTLocalVariableDeclaration) {
1✔
239
            return ((ASTLocalVariableDeclaration) parent).isTypeInferred();
1✔
240
        }
241
        return false;
1✔
242
    }
243

244
    /**
245
     * Receives modifier events in order and checks their order. Methods return
246
     * true if we found a violation and need to stop.
247
     */
248
    interface ModifierOrderEvents {
249

250
        /** Record that the next modifier is the given annotation. */
251
        boolean recordAnnotation(ASTAnnotation annot);
252

253
        /** Record that the next modifier is the given one occurring at the given token. */
254
        boolean recordModifier(JModifier mod, JavaccToken token);
255
    }
256

257
    /**
258
     * Reads a modifier list in order, to recover the order of declared tokens.
259
     * Records annotations and modifiers in source order on the given callback interface.
260
     */
261
    private static void readModifierList(ASTModifierList modList, ModifierOrderEvents events) {
262

263
        JavaccToken tok = modList.getFirstToken();
1✔
264
        final JavaccToken lastTok = modList.getLastToken();
1✔
265

266
        int nextAnnotIndex = 0;
1✔
267
        List<ASTAnnotation> children = modList.children(ASTAnnotation.class).toList();
1✔
268

269
        while (tok != lastTok.getNext()) {
1✔
270
            if (tok.isImplicit()) {
1✔
271
                tok = tok.getNext();
1✔
272
                continue;
1✔
273
            }
274

275
            if (tok.kind == JavaTokenKinds.AT) {
1✔
276
                // this is an annotation
277
                assert nextAnnotIndex < children.size() : "annotation token was not parsed?";
1!
278
                ASTAnnotation annotation = children.get(nextAnnotIndex);
1✔
279
                assert annotation.getFirstToken() == tok : "next annot index didn't match token";
1!
280

281
                nextAnnotIndex++;
1✔
282
                if (events.recordAnnotation(annotation)) {
1✔
283
                    return;
1✔
284
                }
285
                tok = annotation.getLastToken();
1✔
286
            } else {
1✔
287
                JModifier mod = getModFromToken(tok);
1✔
288
                assert mod != null : "Token is not a modifier token? " + tok;
1!
289
                if (events.recordModifier(mod, tok)) {
1✔
290
                    return;
1✔
291
                }
292
                if (mod == JModifier.NON_SEALED) {
1✔
293
                    // advance until the sealed token
294
                    tok = tok.getNext();
1✔
295
                    assert tok.kind == JavaTokenKinds.MINUS;
1!
296
                    tok = tok.getNext();
1✔
297
                    assert tok.kind == JavaTokenKinds.IDENTIFIER && tok.getImageCs().contentEquals("sealed");
1!
298
                }
299
            }
300

301
            tok = tok.getNext();
1✔
302
        }
303

304

305
    }
1✔
306

307
    private static JModifier getModFromToken(JavaccToken tok) {
308
        switch (tok.kind) {
1!
309
        case JavaTokenKinds.PUBLIC:
310
            return JModifier.PUBLIC;
1✔
311
        case JavaTokenKinds.PROTECTED:
312
            return JModifier.PROTECTED;
1✔
313
        case JavaTokenKinds.PRIVATE:
314
            return JModifier.PRIVATE;
1✔
315
        case JavaTokenKinds.STATIC:
316
            return JModifier.STATIC;
1✔
317
        case JavaTokenKinds.FINAL:
318
            return JModifier.FINAL;
1✔
319
        case JavaTokenKinds.ABSTRACT:
320
            return JModifier.ABSTRACT;
1✔
321
        case JavaTokenKinds.SYNCHRONIZED:
322
            return JModifier.SYNCHRONIZED;
1✔
323
        case JavaTokenKinds.NATIVE:
324
            return JModifier.NATIVE;
1✔
325
        case JavaTokenKinds.TRANSIENT:
326
            return JModifier.TRANSIENT;
1✔
327
        case JavaTokenKinds.VOLATILE:
328
            return JModifier.VOLATILE;
1✔
329
        case JavaTokenKinds.STRICTFP:
330
            return JModifier.STRICTFP;
1✔
331
        case JavaTokenKinds._DEFAULT:
332
            return JModifier.DEFAULT;
1✔
333
        case JavaTokenKinds.IDENTIFIER:
334
            if (tok.getImageCs().contentEquals("non")) {
1✔
335
                return JModifier.NON_SEALED;
1✔
336
            } else if (tok.getImageCs().contentEquals("sealed")) {
1!
337
                return JModifier.SEALED;
1✔
338
            }
339
        // fallthrough
340
        default:
341
            return null;
×
342
        }
343
    }
344

345
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc