Skip to content

Commit ec92813

Browse files
authored
refactor: Config class (#18763)
* refactor: Config class * Update @eslint/config-array * Fix lint error * Remove unnecessary validation
1 parent 8781e6f commit ec92813

3 files changed

Lines changed: 297 additions & 207 deletions

File tree

lib/config/config.js

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* @fileoverview The `Config` class
3+
* @author Nicholas C. Zakas
4+
*/
5+
6+
"use strict";
7+
8+
//-----------------------------------------------------------------------------
9+
// Requirements
10+
//-----------------------------------------------------------------------------
11+
12+
const { RuleValidator } = require("./rule-validator");
13+
const { flatConfigSchema, hasMethod } = require("./flat-config-schema");
14+
const { ObjectSchema } = require("@eslint/config-array");
15+
16+
//-----------------------------------------------------------------------------
17+
// Helpers
18+
//-----------------------------------------------------------------------------
19+
20+
const ruleValidator = new RuleValidator();
21+
22+
const severities = new Map([
23+
[0, 0],
24+
[1, 1],
25+
[2, 2],
26+
["off", 0],
27+
["warn", 1],
28+
["error", 2]
29+
]);
30+
31+
/**
32+
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
33+
* @param {string} identifier The identifier to parse.
34+
* @returns {{objectName: string, pluginName: string}} The parts of the plugin
35+
* name.
36+
*/
37+
function splitPluginIdentifier(identifier) {
38+
const parts = identifier.split("/");
39+
40+
return {
41+
objectName: parts.pop(),
42+
pluginName: parts.join("/")
43+
};
44+
}
45+
46+
/**
47+
* Returns the name of an object in the config by reading its `meta` key.
48+
* @param {Object} object The object to check.
49+
* @returns {string?} The name of the object if found or `null` if there
50+
* is no name.
51+
*/
52+
function getObjectId(object) {
53+
54+
// first check old-style name
55+
let name = object.name;
56+
57+
if (!name) {
58+
59+
if (!object.meta) {
60+
return null;
61+
}
62+
63+
name = object.meta.name;
64+
65+
if (!name) {
66+
return null;
67+
}
68+
}
69+
70+
// now check for old-style version
71+
let version = object.version;
72+
73+
if (!version) {
74+
version = object.meta && object.meta.version;
75+
}
76+
77+
// if there's a version then append that
78+
if (version) {
79+
return `${name}@${version}`;
80+
}
81+
82+
return name;
83+
}
84+
85+
/**
86+
* Converts a languageOptions object to a JSON representation.
87+
* @param {Record<string, any>} languageOptions The options to create a JSON
88+
* representation of.
89+
* @param {string} objectKey The key of the object being converted.
90+
* @returns {Record<string, any>} The JSON representation of the languageOptions.
91+
* @throws {TypeError} If a function is found in the languageOptions.
92+
*/
93+
function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
94+
95+
const result = {};
96+
97+
for (const [key, value] of Object.entries(languageOptions)) {
98+
if (value) {
99+
if (typeof value === "object") {
100+
const name = getObjectId(value);
101+
102+
if (name && hasMethod(value)) {
103+
result[key] = name;
104+
} else {
105+
result[key] = languageOptionsToJSON(value, key);
106+
}
107+
continue;
108+
}
109+
110+
if (typeof value === "function") {
111+
throw new TypeError(`Cannot serialize key "${key}" in ${objectKey}: Function values are not supported.`);
112+
}
113+
114+
}
115+
116+
result[key] = value;
117+
}
118+
119+
return result;
120+
}
121+
122+
/**
123+
* Normalizes the rules configuration. Ensure that each rule config is
124+
* an array and that the severity is a number. This function modifies the
125+
* rulesConfig.
126+
* @param {Record<string, any>} rulesConfig The rules configuration to normalize.
127+
* @returns {void}
128+
*/
129+
function normalizeRulesConfig(rulesConfig) {
130+
131+
for (const [ruleId, ruleConfig] of Object.entries(rulesConfig)) {
132+
133+
// ensure rule config is an array
134+
if (!Array.isArray(ruleConfig)) {
135+
rulesConfig[ruleId] = [ruleConfig];
136+
}
137+
138+
// normalize severity
139+
rulesConfig[ruleId][0] = severities.get(rulesConfig[ruleId][0]);
140+
}
141+
142+
}
143+
144+
145+
//-----------------------------------------------------------------------------
146+
// Exports
147+
//-----------------------------------------------------------------------------
148+
149+
/**
150+
* Represents a normalized configuration object.
151+
*/
152+
class Config {
153+
154+
/**
155+
* The name to use for the language when serializing to JSON.
156+
* @type {string|undefined}
157+
*/
158+
#languageName;
159+
160+
/**
161+
* The name to use for the processor when serializing to JSON.
162+
* @type {string|undefined}
163+
*/
164+
#processorName;
165+
166+
/**
167+
* Creates a new instance.
168+
* @param {Object} config The configuration object.
169+
*/
170+
constructor(config) {
171+
172+
const { plugins, language, languageOptions, processor, ...otherKeys } = config;
173+
174+
// Validate config object
175+
const schema = new ObjectSchema(flatConfigSchema);
176+
177+
schema.validate(config);
178+
179+
// first, copy all the other keys over
180+
Object.assign(this, otherKeys);
181+
182+
// ensure that a language is specified
183+
if (!language) {
184+
throw new TypeError("Key 'language' is required.");
185+
}
186+
187+
// copy the rest over
188+
this.plugins = plugins;
189+
this.language = language;
190+
191+
if (languageOptions) {
192+
this.languageOptions = languageOptions;
193+
}
194+
195+
// Check language value
196+
const { pluginName: languagePluginName, objectName: localLanguageName } = splitPluginIdentifier(language);
197+
198+
this.#languageName = language;
199+
200+
if (!plugins || !plugins[languagePluginName] || !plugins[languagePluginName].languages || !plugins[languagePluginName].languages[localLanguageName]) {
201+
throw new TypeError(`Key "language": Could not find "${localLanguageName}" in plugin "${languagePluginName}".`);
202+
}
203+
204+
this.language = plugins[languagePluginName].languages[localLanguageName];
205+
206+
// Validate language options
207+
if (this.languageOptions) {
208+
try {
209+
this.language.validateLanguageOptions(this.languageOptions);
210+
} catch (error) {
211+
throw new TypeError(`Key "languageOptions": ${error.message}`, { cause: error });
212+
}
213+
}
214+
215+
// Check processor value
216+
if (processor) {
217+
this.processor = processor;
218+
219+
if (typeof processor === "string") {
220+
const { pluginName, objectName: localProcessorName } = splitPluginIdentifier(processor);
221+
222+
this.#processorName = processor;
223+
224+
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[localProcessorName]) {
225+
throw new TypeError(`Key "processor": Could not find "${localProcessorName}" in plugin "${pluginName}".`);
226+
}
227+
228+
this.processor = plugins[pluginName].processors[localProcessorName];
229+
} else if (typeof processor === "object") {
230+
this.#processorName = getObjectId(processor);
231+
this.processor = processor;
232+
} else {
233+
throw new TypeError("Key 'processor' must be a string or an object.");
234+
}
235+
}
236+
237+
// Process the rules
238+
if (this.rules) {
239+
normalizeRulesConfig(this.rules);
240+
ruleValidator.validate(this);
241+
}
242+
}
243+
244+
/**
245+
* Converts the configuration to a JSON representation.
246+
* @returns {Record<string, any>} The JSON representation of the configuration.
247+
* @throws {Error} If the configuration cannot be serialized.
248+
*/
249+
toJSON() {
250+
251+
if (this.processor && !this.#processorName) {
252+
throw new Error("Could not serialize processor object (missing 'meta' object).");
253+
}
254+
255+
if (!this.#languageName) {
256+
throw new Error("Could not serialize language object (missing 'meta' object).");
257+
}
258+
259+
return {
260+
...this,
261+
plugins: Object.entries(this.plugins).map(([namespace, plugin]) => {
262+
263+
const pluginId = getObjectId(plugin);
264+
265+
if (!pluginId) {
266+
return namespace;
267+
}
268+
269+
return `${namespace}:${pluginId}`;
270+
}),
271+
language: this.#languageName,
272+
languageOptions: languageOptionsToJSON(this.languageOptions),
273+
processor: this.#processorName
274+
};
275+
}
276+
}
277+
278+
module.exports = { Config };

0 commit comments

Comments
 (0)