Skip to content

Commit 1226d78

Browse files
committed
feat(linter): fill schema with rule configurations (#22907)
This PR is a proof-of-concept to enrich the generated Oxlint configuration JSON schema (and corresponding generated TS types) with per-rule configuration shapes instead of defaulting everything to `DummyRule`, while temporarily skipping rules whose schema generation hasn’t been validated. The 3 rules are new and do not get skipped. (I generated the list from an old commit), verified that the generated output is the expected user configuration for each rule. All other rules are skipped and will be enabled in future PRs.
1 parent 336413b commit 1226d78

6 files changed

Lines changed: 1374 additions & 25 deletions

File tree

apps/oxlint/src-js/package/config.generated.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export type LintPluginOptionsSchema =
5959
| "vue";
6060
export type LintPlugins = LintPluginOptionsSchema[];
6161
export type DummyRule = AllowWarnDeny | [AllowWarnDeny, ...unknown[]];
62+
export type CaseType = "camelCase" | "snake_case";
6263
export type OxlintOverrides = OxlintOverride[];
6364
export type JestVersionSchema = number | string;
6465
export type TagNamePreference =
@@ -1323,15 +1324,19 @@ export interface DummyRuleMap {
13231324
"vue/no-multiple-slot-args"?: DummyRule;
13241325
"vue/no-required-prop-with-default"?: DummyRule;
13251326
"vue/no-reserved-component-names"?: DummyRule;
1326-
"vue/no-reserved-keys"?: DummyRule;
1327-
"vue/no-reserved-props"?: DummyRule;
1327+
"vue/no-reserved-keys"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, NoReservedKeysConfig];
1328+
"vue/no-reserved-props"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, NoReservedPropsConfig];
13281329
"vue/no-shared-component-data"?: DummyRule;
13291330
"vue/no-this-in-before-route-enter"?: DummyRule;
13301331
"vue/no-watch-after-await"?: DummyRule;
13311332
"vue/prefer-import-from-vue"?: DummyRule;
1332-
"vue/prop-name-casing"?: DummyRule;
1333+
"vue/prop-name-casing"?:
1334+
| AllowWarnDeny
1335+
| [AllowWarnDeny]
1336+
| [AllowWarnDeny, CaseType]
1337+
| [AllowWarnDeny, CaseType, Options];
13331338
"vue/require-default-export"?: DummyRule;
1334-
"vue/require-direct-export"?: DummyRule;
1339+
"vue/require-direct-export"?: AllowWarnDeny | [AllowWarnDeny] | [AllowWarnDeny, RequireDirectExport];
13351340
"vue/require-prop-type-constructor"?: DummyRule;
13361341
"vue/require-prop-types"?: DummyRule;
13371342
"vue/require-render-return"?: DummyRule;
@@ -1346,6 +1351,33 @@ export interface DummyRuleMap {
13461351
yoda?: DummyRule;
13471352
[k: string]: DummyRule | undefined;
13481353
}
1354+
export interface NoReservedKeysConfig {
1355+
/**
1356+
* Extra component option groups to inspect, on top of the built-in
1357+
* `props` / `computed` / `data` / `asyncData` / `methods` / `setup`.
1358+
*/
1359+
groups?: string[];
1360+
/**
1361+
* Extra reserved key names to disallow, on top of the built-in list.
1362+
*/
1363+
reserved?: string[];
1364+
}
1365+
export interface NoReservedPropsConfig {
1366+
/**
1367+
* Vue major version whose reserved attribute set is applied. Vue 2 reserves
1368+
* more names (`is`, `slot`, `class`, `style`, ...) than Vue 3.
1369+
*/
1370+
vueVersion?: number;
1371+
}
1372+
export interface Options {
1373+
ignoreProps?: string[];
1374+
}
1375+
export interface RequireDirectExport {
1376+
/**
1377+
* When set `true`, disallow functional component functions.
1378+
*/
1379+
disallowFunctionalComponentFunction?: boolean;
1380+
}
13491381
/**
13501382
* Configure the behavior of linter plugins.
13511383
*

crates/oxc_linter/src/config/rules.rs

Lines changed: 172 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ use schemars::{
88
r#gen::SchemaGenerator,
99
schema::{ArrayValidation, InstanceType, Schema, SchemaObject},
1010
};
11+
12+
#[cfg(feature = "ruledocs")]
13+
use schemars::schema::SingleOrVec;
14+
1115
use serde::{
1216
Deserialize, Serialize, Serializer,
1317
de::{self, Deserializer, Visitor},
@@ -17,6 +21,8 @@ use smallvec::SmallVec;
1721

1822
use oxc_diagnostics::{Error, OxcDiagnostic};
1923

24+
#[cfg(feature = "ruledocs")]
25+
use crate::utils::should_skip_config_schema;
2026
use crate::{
2127
AllowWarnDeny, ExternalPluginStore, LintPlugins,
2228
external_plugin_store::{ExternalOptionsId, ExternalRuleId, ExternalRuleLookupError},
@@ -306,15 +312,173 @@ impl JsonSchema for OxlintRules {
306312
}
307313

308314
fn json_schema(r#gen: &mut SchemaGenerator) -> Schema {
315+
#[cfg(feature = "ruledocs")]
316+
fn resolve_references_in_schema<'a>(
317+
schema: &'a Schema,
318+
r#gen: &'a SchemaGenerator,
319+
) -> &'a Schema {
320+
let mut current = schema;
321+
while let Some(next) = r#gen.dereference(current) {
322+
current = next;
323+
}
324+
325+
let Schema::Object(obj) = current else {
326+
return current;
327+
};
328+
329+
// reuse the defined schema if it's already an object schema,
330+
// we really only need to dereference array schemas for rule config
331+
// TODO: for the array config, the reference should be removed from the generator.
332+
if obj.object.is_some() {
333+
return schema;
334+
}
335+
336+
current
337+
}
338+
339+
// we expect that rules config items does not extend 4,294,967,295 entries.
340+
#[expect(clippy::cast_possible_truncation)]
341+
#[cfg(feature = "ruledocs")]
342+
fn rule_config_schema(r: &RuleEnum, r#gen: &mut SchemaGenerator) -> Schema {
343+
fn with_toggle_schema(
344+
config_schema: Schema,
345+
r#gen: &mut SchemaGenerator,
346+
) -> Schema {
347+
SchemaObject {
348+
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
349+
any_of: Some(vec![
350+
r#gen.subschema_for::<AllowWarnDeny>(),
351+
config_schema,
352+
]),
353+
..Default::default()
354+
})),
355+
..Default::default()
356+
}
357+
.into()
358+
}
359+
360+
let Some(schema) = r.schema(r#gen) else {
361+
return r#gen.subschema_for::<DummyRule>();
362+
};
363+
364+
let schema = resolve_references_in_schema(&schema, r#gen).clone();
365+
366+
let Schema::Object(obj) = schema else {
367+
let array_schema = SchemaObject {
368+
instance_type: Some(InstanceType::Array.into()),
369+
array: Some(Box::new(ArrayValidation {
370+
items: Some(SingleOrVec::Vec(vec![
371+
r#gen.subschema_for::<AllowWarnDeny>(),
372+
Schema::Bool(true),
373+
])),
374+
min_items: Some(1),
375+
max_items: Some(2),
376+
..Default::default()
377+
})),
378+
..Default::default()
379+
}
380+
.into();
381+
return with_toggle_schema(array_schema, r#gen);
382+
};
383+
384+
debug_assert!(
385+
(u8::from(obj.array.is_some())
386+
+ u8::from(obj.object.is_some())
387+
+ u8::from(obj.reference.is_some()))
388+
<= 1,
389+
"Expected rule schema to be either an object, an array, or a reference, but not multiple"
390+
);
391+
392+
if let Some(reference) = obj.reference {
393+
let array_schema = SchemaObject {
394+
instance_type: Some(InstanceType::Array.into()),
395+
array: Some(Box::new(ArrayValidation {
396+
items: Some(SingleOrVec::Vec(vec![
397+
r#gen.subschema_for::<AllowWarnDeny>(),
398+
Schema::Object(SchemaObject {
399+
reference: Some(reference),
400+
..Default::default()
401+
}),
402+
])),
403+
min_items: Some(1),
404+
max_items: Some(2),
405+
..Default::default()
406+
})),
407+
..Default::default()
408+
}
409+
.into();
410+
return with_toggle_schema(array_schema, r#gen);
411+
}
412+
413+
if let Some(array) = obj.array {
414+
let items = match array.items {
415+
None => vec![r#gen.subschema_for::<AllowWarnDeny>()],
416+
Some(SingleOrVec::Single(config)) => {
417+
vec![r#gen.subschema_for::<AllowWarnDeny>(), *config]
418+
}
419+
Some(SingleOrVec::Vec(configs)) => {
420+
let mut items = Vec::with_capacity(configs.len().saturating_add(1));
421+
items.push(r#gen.subschema_for::<AllowWarnDeny>());
422+
items.extend(configs);
423+
items
424+
}
425+
};
426+
427+
let config_length = items.len() as u32;
428+
429+
let array_schema = SchemaObject {
430+
instance_type: Some(InstanceType::Array.into()),
431+
array: Some(Box::new(ArrayValidation {
432+
items: Some(SingleOrVec::Vec(items)),
433+
min_items: Some(1),
434+
max_items: Some(config_length),
435+
..Default::default()
436+
})),
437+
..Default::default()
438+
}
439+
.into();
440+
return with_toggle_schema(array_schema, r#gen);
441+
}
442+
443+
let array_schema = Schema::Object(SchemaObject {
444+
instance_type: Some(InstanceType::Array.into()),
445+
array: Some(Box::new(ArrayValidation {
446+
items: Some(SingleOrVec::Vec(vec![
447+
r#gen.subschema_for::<AllowWarnDeny>(),
448+
Schema::Object(obj),
449+
])),
450+
min_items: Some(1),
451+
max_items: Some(2),
452+
..Default::default()
453+
})),
454+
..Default::default()
455+
});
456+
457+
with_toggle_schema(array_schema, r#gen)
458+
}
459+
460+
let dummy_schema = r#gen.subschema_for::<DummyRule>();
461+
309462
let rules_enum = RULES.iter().map(|r| {
463+
#[cfg(feature = "ruledocs")]
464+
let schema = if should_skip_config_schema(r) {
465+
r#gen.subschema_for::<DummyRule>()
466+
} else {
467+
rule_config_schema(r, r#gen)
468+
};
469+
#[cfg(not(feature = "ruledocs"))]
470+
let schema = r#gen.subschema_for::<DummyRule>();
310471
if r.plugin_name() == "eslint" {
311-
r.name().to_string()
472+
(r.name().to_string(), schema)
312473
} else {
313-
format!(
314-
"{}/{}",
315-
// replace `jsx_a11y` with `jsx-a11y`, `react_perf` with `react-perf`.
316-
r.plugin_name().cow_replace('_', "-"),
317-
r.name()
474+
(
475+
format!(
476+
"{}/{}",
477+
// replace `jsx_a11y` with `jsx-a11y`, `react_perf` with `react-perf`.
478+
r.plugin_name().cow_replace('_', "-"),
479+
r.name()
480+
),
481+
schema,
318482
)
319483
}
320484
});
@@ -329,10 +493,8 @@ impl JsonSchema for OxlintRules {
329493
})),
330494
instance_type: Some(InstanceType::Object.into()),
331495
object: Some(Box::new(schemars::schema::ObjectValidation {
332-
additional_properties: Some(Box::new(r#gen.subschema_for::<DummyRule>())),
333-
properties: rules_enum
334-
.map(|rule_name| (rule_name, r#gen.subschema_for::<DummyRule>()))
335-
.collect(),
496+
additional_properties: Some(Box::new(dummy_schema)),
497+
properties: rules_enum.collect(),
336498
..Default::default()
337499
})),
338500
..Default::default()

crates/oxc_linter/src/rules/eslint/radix.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@ pub struct Radix(RadixType);
3939
enum RadixType {
4040
/// Always require the radix parameter when using `parseInt()`.
4141
#[default]
42-
#[schemars(skip)]
4342
Always,
4443
/// Only require the radix parameter when necessary.
45-
#[schemars(skip)]
4644
AsNeeded,
4745
}
4846

0 commit comments

Comments
 (0)