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

pmd / pmd / #3788

pending completion
#3788

push

github actions

web-flow
Merge pull request #4387 from adangel/pmd7-language-versions

137 of 137 new or added lines in 32 files covered. (100.0%)

67152 of 127777 relevant lines covered (52.55%)

0.53 hits per line

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

80.77
/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.setOf;
8

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

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

25
import net.sourceforge.pmd.util.AssertionUtil;
26
import net.sourceforge.pmd.util.StringUtil;
27

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

35
    private final LanguageMetadata meta;
36

37
    private final List<LanguageVersion> distinctVersions;
38
    private final Map<String, LanguageVersion> byName;
39
    private final LanguageVersion defaultVersion;
40
    private final Set<String> dependencies;
41

42

43
    /**
44
     * Construct a module instance using the given metadata. The metadata must
45
     * be properly constructed.
46
     *
47
     * @throws IllegalStateException If the metadata is invalid (eg missing extensions or name or no versions)
48
     */
49
    protected LanguageModuleBase(LanguageMetadata metadata) {
1✔
50
        this.meta = metadata;
1✔
51
        metadata.validate();
1✔
52
        this.dependencies = Collections.unmodifiableSet(metadata.dependencies);
1✔
53
        List<LanguageVersion> versions = new ArrayList<>();
1✔
54
        Map<String, LanguageVersion> byName = new HashMap<>();
1✔
55
        LanguageVersion defaultVersion = null;
1✔
56

57
        if (metadata.versionMetadata.isEmpty()) {
1✔
58
            throw new IllegalStateException("No versions for '" + getId() + "'");
1✔
59
        }
60

61
        int i = 0;
1✔
62
        for (LanguageMetadata.LangVersionMetadata versionId : metadata.versionMetadata) {
1✔
63
            String versionStr = versionId.name;
1✔
64
            LanguageVersion languageVersion = new LanguageVersion(this, versionStr, i++);
1✔
65

66
            versions.add(languageVersion);
1✔
67

68
            checkNotPresent(byName, versionStr);
1✔
69
            byName.put(versionStr, languageVersion);
1✔
70
            for (String alias : versionId.aliases) {
1✔
71
                checkNotPresent(byName, alias);
1✔
72
                byName.put(alias, languageVersion);
1✔
73
            }
1✔
74

75
            if (versionId.isDefault) {
1✔
76
                if (defaultVersion != null) {
1✔
77
                    throw new IllegalStateException(
×
78
                        "Default version already set to " + defaultVersion + ", cannot set it to " + languageVersion);
79
                }
80
                defaultVersion = languageVersion;
1✔
81
            }
82
        }
1✔
83

84
        this.byName = Collections.unmodifiableMap(byName);
1✔
85
        this.distinctVersions = Collections.unmodifiableList(versions);
1✔
86
        this.defaultVersion = Objects.requireNonNull(defaultVersion, "No default version for " + getId());
1✔
87

88
    }
1✔
89

90

91
    private static void checkNotPresent(Map<String, ?> map, String alias) {
92
        if (map.containsKey(alias)) {
1✔
93
            throw new IllegalArgumentException("Version key '" + alias + "' is duplicated");
×
94
        }
95
    }
1✔
96

97
    @Override
98
    public List<LanguageVersion> getVersions() {
99
        return distinctVersions;
1✔
100
    }
101

102
    @Override
103
    public LanguageVersion getDefaultVersion() {
104
        return defaultVersion;
1✔
105
    }
106

107
    @Override
108
    public LanguageVersion getVersion(String version) {
109
        return byName.get(version);
1✔
110
    }
111

112
    @Override
113
    public Set<String> getVersionNamesAndAliases() {
114
        return Collections.unmodifiableSet(byName.keySet());
×
115
    }
116

117
    @Override
118
    public Set<String> getDependencies() {
119
        return dependencies;
1✔
120
    }
121

122
    @Override
123
    public String getName() {
124
        return meta.name;
1✔
125
    }
126

127
    @Override
128
    public String getShortName() {
129
        return meta.getShortName();
×
130
    }
131

132
    @Override
133
    public String getTerseName() {
134
        return meta.id;
1✔
135
    }
136

137
    @Override
138
    public @NonNull List<String> getExtensions() {
139
        return Collections.unmodifiableList(meta.extensions);
1✔
140
    }
141

142
    @Override
143
    public String toString() {
144
        return getTerseName();
1✔
145
    }
146

147
    @Override
148
    public int compareTo(Language o) {
149
        return getName().compareTo(o.getName());
1✔
150
    }
151

152
    @Override
153
    public int hashCode() {
154
        return Objects.hash(getId());
1✔
155
    }
156

157
    @Override
158
    public boolean equals(Object obj) {
159
        if (this == obj) {
1✔
160
            return true;
1✔
161
        }
162
        if (obj == null) {
1✔
163
            return false;
×
164
        }
165
        if (getClass() != obj.getClass()) {
1✔
166
            return false;
1✔
167
        }
168
        LanguageModuleBase other = (LanguageModuleBase) obj;
×
169
        return Objects.equals(getId(), other.getId());
×
170
    }
171

172
    /**
173
     * Metadata about a language, basically a builder pattern for the
174
     * language instance.
175
     *
176
     * <p>Some of the metadata are mandatory:
177
     * <ul>
178
     * <li>The id ({@link #withId(String)})
179
     * <li>The display name ({@link #name(String)})
180
     * <li>The file extensions ({@link #extensions(String, String...)}
181
     * </ul>
182
     *
183
     */
184
    protected static final class LanguageMetadata {
185

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

190
        private final Set<String> dependencies = new HashSet<>();
1✔
191
        private String name;
192
        private @Nullable String shortName;
193
        private final @NonNull String id;
194
        private List<String> extensions;
195
        private final List<LangVersionMetadata> versionMetadata = new ArrayList<>();
1✔
196

197
        private LanguageMetadata(@NonNull String id) {
1✔
198
            this.id = id;
1✔
199
            if (!VALID_LANG_ID.matcher(id).matches()) {
1✔
200
                throw new IllegalArgumentException(
1✔
201
                    "ID '" + id + "' is not a valid language ID (should match " + VALID_LANG_ID + ").");
202
            }
203
        }
1✔
204

205
        void validate() {
206
            AssertionUtil.validateState(name != null, "Language " + id + " should have a name");
1✔
207
            AssertionUtil.validateState(
1✔
208
                extensions != null, "Language " + id + " has not registered any file extensions");
209
        }
1✔
210

211
        String getShortName() {
212
            return shortName == null ? name : shortName;
1✔
213
        }
214

215
        /**
216
         * Factory method to create an ID.
217
         *
218
         * @param id The language id. Must be usable as a Java package name segment,
219
         *           ie be lowercase, alphanumeric, starting with a letter.
220
         *
221
         * @return A builder for language metadata
222
         *
223
         * @throws IllegalArgumentException If the parameter is not a valid ID
224
         * @throws NullPointerException     If the parameter is null
225
         */
226
        public static LanguageMetadata withId(@NonNull String id) {
227
            return new LanguageMetadata(id);
1✔
228
        }
229

230
        /**
231
         * Record the {@linkplain Language#getName() display name} of
232
         * the language. This also serves as the {@linkplain Language#getShortName() short name}
233
         * if {@link #shortName(String)} is not called.
234
         *
235
         * @param name Display name of the language
236
         *
237
         * @throws NullPointerException     If the parameter is null
238
         * @throws IllegalArgumentException If the parameter is not a valid language name
239
         */
240
        public LanguageMetadata name(@NonNull String name) {
241
            AssertionUtil.requireParamNotNull("name", name);
1✔
242
            if (StringUtils.isBlank(name)) {
1✔
243
                throw new IllegalArgumentException("Not a valid language name: " + StringUtil.inSingleQuotes(name));
×
244
            }
245
            this.name = name.trim();
1✔
246
            return this;
1✔
247
        }
248

249
        /**
250
         * Record the {@linkplain Language#getShortName() short name} of the language.
251
         *
252
         * @param shortName Short name of the language
253
         *
254
         * @throws NullPointerException     If the parameter is null
255
         * @throws IllegalArgumentException If the parameter is not a valid language name
256
         */
257

258
        public LanguageMetadata shortName(@NonNull String shortName) {
259
            AssertionUtil.requireParamNotNull("short name", shortName);
×
260
            if (StringUtils.isBlank(name)) {
×
261
                throw new IllegalArgumentException("Not a valid language name: " + StringUtil.inSingleQuotes(name));
×
262
            }
263
            this.shortName = shortName.trim();
×
264
            return this;
×
265
        }
266

267
        /**
268
         * Record the {@linkplain Language#getExtensions() extensions}
269
         * assigned to the language. Extensions should not start with a period
270
         * {@code .}.
271
         *
272
         * @param e1     First extensions
273
         * @param others Other extensions (optional)
274
         *
275
         * @throws NullPointerException If any extension is null
276
         */
277
        public LanguageMetadata extensions(String e1, String... others) {
278
            this.extensions = new ArrayList<>(setOf(e1, others));
1✔
279
            AssertionUtil.requireContainsNoNullValue("extensions", this.extensions);
1✔
280
            return this;
1✔
281
        }
282

283
        /**
284
         * Record the {@linkplain Language#getExtensions() extensions}
285
         * assigned to the language. Extensions should not start with a period
286
         * {@code .}. At least one extension must be provided.
287
         *
288
         * @param extensions the extensions
289
         *
290
         * @throws NullPointerException If any extension is null
291
         * @throws IllegalArgumentException If no extensions are provided
292
         */
293
        public LanguageMetadata extensions(Collection<String> extensions) {
294
            this.extensions = new ArrayList<>(new HashSet<>(extensions));
×
295
            AssertionUtil.requireContainsNoNullValue("extensions", this.extensions);
×
296
            if (this.extensions.isEmpty()) {
×
297
                throw new IllegalArgumentException("At least one extension is required.");
×
298
            }
299
            return this;
×
300
        }
301

302
        /**
303
         * Add a new version by its name.
304
         *
305
         * @param name    Version name. Must contain no spaces.
306
         * @param aliases Additional names that are mapped to this version. Must contain no spaces.
307
         *
308
         * @throws NullPointerException     If any parameter is null
309
         * @throws IllegalArgumentException If the name or aliases are empty or contain spaces
310
         */
311

312
        public LanguageMetadata addVersion(String name, String... aliases) {
313
            versionMetadata.add(new LangVersionMetadata(name, Arrays.asList(aliases), false));
1✔
314
            return this;
1✔
315
        }
316

317
        /**
318
         * Add a new version by its name and make it the default version.
319
         *
320
         * @param name    Version name. Must contain no spaces.
321
         * @param aliases Additional names that are mapped to this version. Must contain no spaces.
322
         *
323
         * @throws NullPointerException     If any parameter is null
324
         * @throws IllegalArgumentException If the name or aliases are empty or contain spaces
325
         */
326
        public LanguageMetadata addDefaultVersion(String name, String... aliases) {
327
            versionMetadata.add(new LangVersionMetadata(name, Arrays.asList(aliases), true));
1✔
328
            return this;
1✔
329
        }
330

331
        /**
332
         * Record that this language depends on another language, identified
333
         * by its id. This means any {@link LanguageProcessorRegistry} that
334
         * contains a processor for this language is asserted upon construction
335
         * to also contain a processor for the language depended on.
336
         *
337
         * @param id ID of the language to depend on.
338
         *
339
         * @throws NullPointerException     If any parameter is null
340
         * @throws IllegalArgumentException If the name is not a valid language Id
341
         */
342

343
        public LanguageMetadata dependsOnLanguage(String id) {
344
            if (!VALID_LANG_ID.matcher(id).matches()) {
1✔
345
                throw new IllegalArgumentException(
1✔
346
                    "ID '" + id + "' is not a valid language ID (should match " + VALID_LANG_ID + ").");
347
            }
348
            dependencies.add(id);
×
349
            return this;
×
350
        }
351

352
        static final class LangVersionMetadata {
353

354
            final String name;
355
            final List<String> aliases;
356
            final boolean isDefault;
357

358
            private LangVersionMetadata(String name, List<String> aliases, boolean isDefault) {
1✔
359
                checkVersionName(name);
1✔
360
                for (String alias : aliases) {
1✔
361
                    checkVersionName(alias);
1✔
362
                }
1✔
363

364
                this.name = name;
1✔
365
                this.aliases = aliases;
1✔
366
                this.isDefault = isDefault;
1✔
367
            }
1✔
368

369
            private static void checkVersionName(String name) {
370
                if (StringUtils.isBlank(name) || SPACE_PAT.matcher(name).find()) {
1✔
371
                    throw new IllegalArgumentException("Invalid version name: " + StringUtil.inSingleQuotes(name));
1✔
372
                }
373
            }
1✔
374
        }
375
    }
376
}
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

© 2025 Coveralls, Inc