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

pmd / pmd / 152

11 Sep 2025 09:46AM UTC coverage: 78.65% (+0.05%) from 78.603%
152

push

github

web-flow
[java] New rule: ModifierOrder (#5601)

18160 of 23935 branches covered (75.87%)

Branch coverage included in aggregate %.

175 of 182 new or added lines in 10 files covered. (96.15%)

2 existing lines in 2 files now uncovered.

39628 of 49540 relevant lines covered (79.99%)

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
            default:
NEW
66
                throw AssertionUtil.shouldNotReachHere("exhaustive switch");
×
67
            }
68
        }
69
    }
70

71
    private TypeAnnotationPosition typeAnnotPosition;
72

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

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

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

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

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

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

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

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

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

122
        }
123

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

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

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

141

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

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

159
            return false;
1✔
160
        }
161

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

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

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

180
            private @Nullable LastModSeen lastModSeen;
181

182

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

305

306
    }
1✔
307

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

346
}
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