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

pmd / pmd / 380

29 Jan 2026 03:55PM UTC coverage: 78.964%. Remained the same
380

push

github

adangel
[doc] ADR 3: Clarify javadoc tags (#6392)

18537 of 24358 branches covered (76.1%)

Branch coverage included in aggregate %.

40391 of 50268 relevant lines covered (80.35%)

0.81 hits per line

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

73.99
/pmd-core/src/main/java/net/sourceforge/pmd/lang/LanguageModuleBase.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang;
6

7
import static net.sourceforge.pmd.util.CollectionUtil.emptyList;
8
import static net.sourceforge.pmd.util.CollectionUtil.setOf;
9

10
import java.util.ArrayList;
11
import java.util.Arrays;
12
import java.util.Collection;
13
import java.util.Collections;
14
import java.util.HashMap;
15
import java.util.HashSet;
16
import java.util.List;
17
import java.util.Map;
18
import java.util.Objects;
19
import java.util.Set;
20
import java.util.regex.Pattern;
21

22
import org.apache.commons.lang3.StringUtils;
23
import org.checkerframework.checker.nullness.qual.NonNull;
24
import org.checkerframework.checker.nullness.qual.Nullable;
25

26
import net.sourceforge.pmd.annotation.Experimental;
27
import net.sourceforge.pmd.lang.LanguageModuleBase.LanguageMetadata.LangVersionMetadata;
28
import net.sourceforge.pmd.util.AssertionUtil;
29
import net.sourceforge.pmd.util.StringUtil;
30

31
/**
32
 * Base class for language modules.
33
 *
34
 * @author Clément Fournier
35
 */
36
public abstract class LanguageModuleBase implements Language {
37

38
    private final LanguageMetadata meta;
39

40
    private final List<LanguageVersion> distinctVersions;
41
    private final Map<String, LanguageVersion> byName;
42
    private final LanguageVersion defaultVersion;
43
    private final Set<String> dependencies;
44
    private final @Nullable String baseLanguageId;
45

46

47
    /**
48
     * Construct a module instance using the given metadata. The metadata must
49
     * be properly constructed.
50
     *
51
     * @throws IllegalStateException If the metadata is invalid (eg missing extensions or name or no versions)
52
     */
53
    protected LanguageModuleBase(LanguageMetadata metadata) {
54
        this(metadata, null);
1✔
55
    }
1✔
56

57
    /**
58
     * @since 7.13.0
59
     * @experimental See <a href="https://github.com/pmd/pmd/pull/5438">[core] Support language dialects #5438</a>.
60
     */
61
    @Experimental
62
    protected LanguageModuleBase(DialectLanguageMetadata metadata) {
63
        this(metadata.metadata, metadata.baseLanguageId);
1✔
64
    }
1✔
65

66
    private LanguageModuleBase(LanguageMetadata metadata, String baseLanguageId) {
1✔
67
        this.meta = metadata;
1✔
68
        metadata.validate();
1✔
69
        this.dependencies = Collections.unmodifiableSet(metadata.dependencies);
1✔
70
        this.baseLanguageId = baseLanguageId;
1✔
71

72
        List<LanguageVersion> versions = new ArrayList<>();
1✔
73
        Map<String, LanguageVersion> byName = new HashMap<>();
1✔
74
        LanguageVersion defaultVersion = null;
1✔
75

76
        if (metadata.versionMetadata.isEmpty()) {
1✔
77
            if (this instanceof PmdCapableLanguage) {
1!
78
                // pmd languages need to have versions
79
                throw new IllegalStateException("No versions for '" + getId() + "'");
1✔
80
            } else {
81
                // for others, a version is declared implicitly
82
                metadata.versionMetadata.add(new LangVersionMetadata());
×
83
            }
84
        }
85

86
        int i = 0;
1✔
87
        for (LanguageMetadata.LangVersionMetadata versionId : metadata.versionMetadata) {
1✔
88
            String versionStr = versionId.name;
1✔
89
            LanguageVersion languageVersion = new LanguageVersion(this, versionStr, i++, versionId.aliases);
1✔
90

91
            versions.add(languageVersion);
1✔
92

93
            checkNotPresent(byName, versionStr);
1✔
94
            byName.put(versionStr, languageVersion);
1✔
95
            for (String alias : versionId.aliases) {
1✔
96
                checkNotPresent(byName, alias);
1✔
97
                byName.put(alias, languageVersion);
1✔
98
            }
1✔
99

100
            if (versionId.isDefault) {
1✔
101
                if (defaultVersion != null) {
1!
102
                    throw new IllegalStateException(
×
103
                        "Default version already set to " + defaultVersion + ", cannot set it to " + languageVersion);
104
                }
105
                defaultVersion = languageVersion;
1✔
106
            }
107
        }
1✔
108

109
        this.byName = Collections.unmodifiableMap(byName);
1✔
110
        this.distinctVersions = Collections.unmodifiableList(versions);
1✔
111
        this.defaultVersion = Objects.requireNonNull(defaultVersion, "No default version for " + getId());
1✔
112
    }
1✔
113

114
    private static void checkNotPresent(Map<String, ?> map, String alias) {
115
        if (map.containsKey(alias)) {
1!
116
            throw new IllegalArgumentException("Version key '" + alias + "' is duplicated");
×
117
        }
118
    }
1✔
119

120
    @Override
121
    public @Nullable String getBaseLanguageId() {
122
        return baseLanguageId;
1✔
123
    }
124

125
    @Override
126
    public List<LanguageVersion> getVersions() {
127
        return distinctVersions;
1✔
128
    }
129

130
    @Override
131
    public @NonNull LanguageVersion getDefaultVersion() {
132
        return defaultVersion;
1✔
133
    }
134

135
    @Override
136
    public LanguageVersion getVersion(String version) {
137
        return byName.get(version);
1✔
138
    }
139

140
    @Override
141
    public Set<String> getVersionNamesAndAliases() {
142
        return Collections.unmodifiableSet(byName.keySet());
×
143
    }
144

145
    @Override
146
    public Set<String> getDependencies() {
147
        return dependencies;
1✔
148
    }
149

150
    @Override
151
    public String getName() {
152
        return meta.name;
1✔
153
    }
154

155
    @Override
156
    public String getShortName() {
157
        return meta.getShortName();
×
158
    }
159

160
    @Override
161
    public String getId() {
162
        return meta.id;
1✔
163
    }
164

165
    @Override
166
    public @NonNull List<String> getExtensions() {
167
        return Collections.unmodifiableList(meta.extensions);
1✔
168
    }
169

170
    @Override
171
    public String toString() {
172
        return getId();
1✔
173
    }
174

175
    @Override
176
    public int compareTo(Language o) {
177
        return getName().compareTo(o.getName());
1✔
178
    }
179

180
    @Override
181
    public int hashCode() {
182
        return getId().hashCode();
1✔
183
    }
184

185
    @Override
186
    public boolean equals(Object obj) {
187
        if (this == obj) {
1✔
188
            return true;
1✔
189
        }
190
        if (obj == null) {
1!
191
            return false;
×
192
        }
193
        if (getClass() != obj.getClass()) {
1!
194
            return false;
1✔
195
        }
196
        LanguageModuleBase other = (LanguageModuleBase) obj;
×
197
        return Objects.equals(getId(), other.getId());
×
198
    }
199

200
    /**
201
     * Metadata about a language, basically a builder pattern for the
202
     * language instance.
203
     *
204
     * <p>Some of the metadata are mandatory:
205
     * <ul>
206
     * <li>The id ({@link #withId(String)})
207
     * <li>The display name ({@link #name(String)})
208
     * <li>The file extensions ({@link #extensions(String, String...)}
209
     * </ul>
210
     */
211
    public static final class LanguageMetadata {
212

213
        /** Language IDs should be conventional Java package names. */
214
        private static final Pattern VALID_LANG_ID = Pattern.compile("[a-z][_a-z0-9]*");
1✔
215
        private static final Pattern SPACE_PAT = Pattern.compile("\\s");
1✔
216

217
        private final Set<String> dependencies = new HashSet<>();
1✔
218
        private String name;
219
        private @Nullable String shortName;
220
        private final @NonNull String id;
221
        private List<String> extensions;
222
        private final List<LangVersionMetadata> versionMetadata = new ArrayList<>();
1✔
223

224
        private LanguageMetadata(@NonNull String id) {
1✔
225
            this.id = id;
1✔
226
            checkValidLangId(id);
1✔
227
        }
1✔
228

229
        void validate() {
230
            AssertionUtil.validateState(name != null, "Language " + id + " should have a name");
1!
231
            AssertionUtil.validateState(
1✔
232
                extensions != null, "Language " + id + " has not registered any file extensions");
233
        }
1✔
234

235
        String getShortName() {
236
            return shortName == null ? name : shortName;
1!
237
        }
238

239
        /**
240
         * Factory method to create an ID.
241
         *
242
         * @param id The language id. Must be usable as a Java package name segment,
243
         *           ie be lowercase, alphanumeric, starting with a letter.
244
         *
245
         * @return A builder for language metadata
246
         *
247
         * @throws IllegalArgumentException If the parameter is not a valid ID
248
         * @throws NullPointerException     If the parameter is null
249
         */
250
        public static LanguageMetadata withId(@NonNull String id) {
251
            return new LanguageMetadata(id);
1✔
252
        }
253

254
        /**
255
         * Record the {@linkplain Language#getName() display name} of
256
         * the language. This also serves as the {@linkplain Language#getShortName() short name}
257
         * if {@link #shortName(String)} is not called.
258
         *
259
         * @param name Display name of the language
260
         *
261
         * @throws NullPointerException     If the parameter is null
262
         * @throws IllegalArgumentException If the parameter is not a valid language name
263
         */
264
        public LanguageMetadata name(@NonNull String name) {
265
            AssertionUtil.requireParamNotNull("name", name);
1✔
266
            if (StringUtils.isBlank(name)) {
1!
267
                throw new IllegalArgumentException("Not a valid language name: " + StringUtil.inSingleQuotes(name));
×
268
            }
269
            this.name = name.trim();
1✔
270
            return this;
1✔
271
        }
272

273
        /**
274
         * Record the {@linkplain Language#getShortName() short name} of the language.
275
         *
276
         * @param shortName Short name of the language
277
         *
278
         * @throws NullPointerException     If the parameter is null
279
         * @throws IllegalArgumentException If the parameter is not a valid language name
280
         */
281

282
        public LanguageMetadata shortName(@NonNull String shortName) {
283
            AssertionUtil.requireParamNotNull("short name", shortName);
×
284
            if (StringUtils.isBlank(name)) {
×
285
                throw new IllegalArgumentException("Not a valid language name: " + StringUtil.inSingleQuotes(name));
×
286
            }
287
            this.shortName = shortName.trim();
×
288
            return this;
×
289
        }
290

291
        /**
292
         * Record the {@linkplain Language#getExtensions() extensions}
293
         * assigned to the language. Extensions should not start with a period
294
         * {@code .}.
295
         *
296
         * @param extensionWithoutPeriod First extensions
297
         * @param others                 Other extensions (optional)
298
         *
299
         * @throws NullPointerException If any extension is null
300
         */
301
        public LanguageMetadata extensions(String extensionWithoutPeriod, String... others) {
302
            this.extensions = new ArrayList<>(setOf(extensionWithoutPeriod, others));
1✔
303
            AssertionUtil.requireContainsNoNullValue("extensions", this.extensions);
1✔
304
            return this;
1✔
305
        }
306

307
        /**
308
         * Record the {@linkplain Language#getExtensions() extensions}
309
         * assigned to the language. Extensions should not start with a period
310
         * {@code .}. At least one extension must be provided.
311
         *
312
         * @param extensions the extensions
313
         *
314
         * @throws NullPointerException     If any extension is null
315
         * @throws IllegalArgumentException If no extensions are provided
316
         */
317
        public LanguageMetadata extensions(Collection<String> extensions) {
318
            this.extensions = new ArrayList<>(new HashSet<>(extensions));
×
319
            AssertionUtil.requireContainsNoNullValue("extensions", this.extensions);
×
320
            if (this.extensions.isEmpty()) {
×
321
                throw new IllegalArgumentException("At least one extension is required.");
×
322
            }
323
            return this;
×
324
        }
325

326
        /**
327
         * Add a new version by its name.
328
         *
329
         * @param name    Version name. Must contain no spaces.
330
         * @param aliases Additional names that are mapped to this version. Must contain no spaces.
331
         *
332
         * @throws NullPointerException     If any parameter is null
333
         * @throws IllegalArgumentException If the name or aliases are empty or contain spaces
334
         */
335

336
        public LanguageMetadata addVersion(String name, String... aliases) {
337
            versionMetadata.add(new LangVersionMetadata(name, Arrays.asList(aliases), false));
1✔
338
            return this;
1✔
339
        }
340

341
        /**
342
         * Add a new version by its name and make it the default version.
343
         *
344
         * @param name    Version name. Must contain no spaces.
345
         * @param aliases Additional names that are mapped to this version. Must contain no spaces.
346
         *
347
         * @throws NullPointerException     If any parameter is null
348
         * @throws IllegalArgumentException If the name or aliases are empty or contain spaces
349
         */
350
        public LanguageMetadata addDefaultVersion(String name, String... aliases) {
351
            versionMetadata.add(new LangVersionMetadata(name, Arrays.asList(aliases), true));
1✔
352
            return this;
1✔
353
        }
354

355

356
        /**
357
         * Add all the versions of the given language, including the
358
         * default version.
359
         *
360
         * @param language Other language
361
         *
362
         * @throws NullPointerException     If any parameter is null
363
         * @throws IllegalArgumentException If the name or aliases are empty or contain spaces
364
         */
365
        public LanguageMetadata addAllVersionsOf(Language language) {
366
            for (LanguageVersion version : language.getVersions()) {
×
367
                versionMetadata.add(new LangVersionMetadata(version.getVersion(),
×
368
                                                            version.getAliases(),
×
369
                                                            version.equals(language.getDefaultVersion())));
×
370
            }
×
371
            return this;
×
372
        }
373

374
        /**
375
         * Defines the language as a dialect of another language.
376
         *
377
         * @param baseLanguageId The id of the base language this is a dialect of.
378
         * @return A new dialect language metadata model.
379
         * @since 7.13.0
380
         * @experimental See <a href="https://github.com/pmd/pmd/pull/5438">[core] Support language dialects #5438</a>.
381
         */
382
        @Experimental
383
        public DialectLanguageMetadata asDialectOf(String baseLanguageId) {
384
            checkValidLangId(baseLanguageId);
1✔
385
            dependsOnLanguage(baseLanguageId); // a dialect automatically depends on it's base language at runtime
1✔
386
            return new DialectLanguageMetadata(this, baseLanguageId);
1✔
387
        }
388

389
        private static void checkValidLangId(String id) {
390
            if (!VALID_LANG_ID.matcher(id).matches()) {
1✔
391
                throw new IllegalArgumentException(
1✔
392
                    "ID '" + id + "' is not a valid language ID (should match " + VALID_LANG_ID + ").");
393
            }
394
        }
1✔
395

396
        /**
397
         * Record that this language depends on another language, identified
398
         * by its id. This means any {@link LanguageProcessorRegistry} that
399
         * contains a processor for this language is asserted upon construction
400
         * to also contain a processor for the language depended on.
401
         *
402
         * @param id ID of the language to depend on.
403
         *
404
         * @throws NullPointerException     If any parameter is null
405
         * @throws IllegalArgumentException If the name is not a valid language Id
406
         */
407

408
        public LanguageMetadata dependsOnLanguage(String id) {
409
            checkValidLangId(id);
1✔
410
            dependencies.add(id);
1✔
411
            return this;
1✔
412
        }
413

414
        static final class LangVersionMetadata {
415

416
            final String name;
417
            final List<String> aliases;
418
            final boolean isDefault;
419

420
            private LangVersionMetadata() {
×
421
                this.name = "";
×
422
                this.aliases = emptyList();
×
423
                this.isDefault = true;
×
424
            }
×
425

426
            private LangVersionMetadata(String name, List<String> aliases, boolean isDefault) {
1✔
427
                checkVersionName(name);
1✔
428
                for (String alias : aliases) {
1✔
429
                    checkVersionName(alias);
1✔
430
                }
1✔
431

432
                this.name = name;
1✔
433
                this.aliases = aliases;
1✔
434
                this.isDefault = isDefault;
1✔
435
            }
1✔
436

437
            private static void checkVersionName(String name) {
438
                if (StringUtils.isBlank(name) || SPACE_PAT.matcher(name).find()) {
1!
439
                    throw new IllegalArgumentException("Invalid version name: " + StringUtil.inSingleQuotes(name));
1✔
440
                }
441
            }
1✔
442
        }
443
    }
444

445
    /**
446
     * Expresses the language as a dialect of another language.
447
     * @since 7.13.0
448
     * @experimental See <a href="https://github.com/pmd/pmd/pull/5438">[core] Support language dialects #5438</a>.
449
     */
450
    @Experimental
451
    public static final class DialectLanguageMetadata {
452
        private final @NonNull LanguageMetadata metadata;
453
        private final @NonNull String baseLanguageId;
454

455
        private DialectLanguageMetadata(LanguageMetadata metadata, String baseLanguageId) {
1✔
456
            this.metadata = metadata;
1✔
457
            this.baseLanguageId = baseLanguageId;
1✔
458
        }
1✔
459
    }
460
}
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