Skip to content

Commit 005ec25

Browse files
Copilotcamc314autofix-ci[bot]
authored
fix(linter): permit $schema .oxlintrc.json struct (#17060)
## Fix: Allow `$schema` in .oxlintrc.json while maintaining deny_unknown_fields ### Problem - The `Oxlintrc` struct has `#[serde(deny_unknown_fields)]` which rejects all unknown fields - Users commonly use `$schema` in their `.oxlintrc.json` files for IDE support (as shown in documentation examples) - This causes deserialization to fail ### Solution Added a `schema` field to the `Oxlintrc` struct that: - Is properly named `$schema` via `#[serde(rename = "$schema")]` - Is optional (`Option<String>`) and skips serialization when `None` - Maintains `deny_unknown_fields` for other unknown properties - Follows the same merge behavior as other optional fields (self takes priority) ### Changes 1. **Added `schema` field to `Oxlintrc` struct** with appropriate serde attributes 2. **Removed manual `$schema` injection** from `generate_schema_json()` - now auto-generated 3. **Updated `merge()` function** to handle the new field correctly 4. **Added comprehensive tests** for deserialization, serialization, and merge behavior 5. **Updated schema JSON** - `$schema` now appears first and has correct type `["string", "null"]` 6. **Fixed CI failures**: - Resolved clippy warning about unnecessary raw string hashes - Updated website_linter snapshot to include `$schema` field ### Testing ✅ Config files with `$schema` are now accepted ✅ Config files without `$schema` still work ✅ Unknown fields are still properly rejected ✅ Merge behavior gives priority to self over other ✅ All existing tests pass (878 tests) ✅ Schema generation produces correct output ✅ Clippy checks pass ✅ Website linter tests pass <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>linter: `$schema` in .oxlintrc.json causes failure to deserailize</issue_title> > <issue_description>cc @connorshea > > https://github.com/oxc-project/oxc-ecosystem-ci/actions/runs/20336440475/job/58424680576</issue_description> > > <agent_instructions>This broke in d446c43, we should keep deny unknonw fields, but allow $schema</agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #17059 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: camc314 <18101008+camc314@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cameron <cameron.clark@hey.com>
1 parent fd03131 commit 005ec25

File tree

4 files changed

+68
-27
lines changed

4 files changed

+68
-27
lines changed

crates/oxc_linter/src/config/oxlintrc.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ use super::{
6868
#[serde(default, deny_unknown_fields)]
6969
#[non_exhaustive]
7070
pub struct Oxlintrc {
71+
/// Schema URI for editor tooling.
72+
#[serde(rename = "$schema", default, skip_serializing_if = "Option::is_none")]
73+
pub schema: Option<String>,
7174
/// Enabled built-in plugins for Oxlint.
7275
/// You can view the list of available plugins on
7376
/// [the website](https://oxc.rs/docs/guide/usage/linter/plugins.html#supported-plugins).
@@ -221,23 +224,6 @@ impl Oxlintrc {
221224

222225
let mut json = serde_json::to_value(&schema).unwrap();
223226

224-
// inject "$schema" at the root for editor support without changing the struct
225-
if let serde_json::Value::Object(map) = &mut json {
226-
let props = map
227-
.entry("properties")
228-
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
229-
if let serde_json::Value::Object(props) = props {
230-
props.insert(
231-
"$schema".to_string(),
232-
serde_json::json!({
233-
"type": "string",
234-
"description": "Schema URI for editor tooling",
235-
"markdownDescription": "Schema URI for editor tooling"
236-
}),
237-
);
238-
}
239-
}
240-
241227
// Inject markdown descriptions for better editor support
242228
Self::inject_markdown_descriptions(&mut json);
243229

@@ -327,7 +313,10 @@ impl Oxlintrc {
327313
(None, None) => None,
328314
};
329315

316+
let schema = self.schema.clone().or(other.schema);
317+
330318
Oxlintrc {
319+
schema,
331320
plugins,
332321
external_plugins,
333322
categories,
@@ -481,4 +470,42 @@ mod test {
481470
let merged = config1.merge(config2);
482471
assert_eq!(merged.external_plugins.unwrap().len(), 2);
483472
}
473+
474+
#[test]
475+
fn test_oxlintrc_schema_field() {
476+
// Test that $schema field is accepted and deserialized correctly
477+
let config: Oxlintrc = serde_json::from_str(
478+
r#"{
479+
"$schema": "./node_modules/oxlint/configuration_schema.json",
480+
"rules": {
481+
"no-console": "warn"
482+
}
483+
}"#,
484+
)
485+
.unwrap();
486+
assert_eq!(
487+
config.schema,
488+
Some("./node_modules/oxlint/configuration_schema.json".to_string())
489+
);
490+
491+
// Test that config without $schema still works
492+
let config_without_schema: Oxlintrc = serde_json::from_str(r#"{"rules": {}}"#).unwrap();
493+
assert_eq!(config_without_schema.schema, None);
494+
495+
// Test serialization - $schema should be skipped when None
496+
let serialized = serde_json::to_string(&config_without_schema).unwrap();
497+
assert!(!serialized.contains("$schema"));
498+
499+
// Test merge - self takes priority over other
500+
let config1: Oxlintrc = serde_json::from_str(r#"{"$schema": "schema1.json"}"#).unwrap();
501+
let config2: Oxlintrc = serde_json::from_str(r#"{"$schema": "schema2.json"}"#).unwrap();
502+
let merged = config1.merge(config2);
503+
assert_eq!(merged.schema, Some("schema1.json".to_string()));
504+
505+
// Test merge - when self has no schema, use other's schema
506+
let config1: Oxlintrc = serde_json::from_str(r"{}").unwrap();
507+
let config2: Oxlintrc = serde_json::from_str(r#"{"$schema": "schema2.json"}"#).unwrap();
508+
let merged = config1.merge(config2);
509+
assert_eq!(merged.schema, Some("schema2.json".to_string()));
510+
}
484511
}

crates/oxc_linter/src/snapshots/schema_json.snap

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ expression: json
88
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json\n{\n\"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n\"plugins\": [\"import\", \"typescript\", \"unicorn\"],\n\"env\": {\n\"browser\": true\n},\n\"globals\": {\n\"foo\": \"readonly\"\n},\n\"settings\": {\n},\n\"rules\": {\n\"eqeqeq\": \"warn\",\n\"import/no-cycle\": \"error\",\n\"react/self-closing-comp\": [\"error\", { \"html\": false }]\n},\n\"overrides\": [\n{\n\"files\": [\"*.test.ts\", \"*.spec.ts\"],\n\"rules\": {\n\"@typescript-eslint/no-explicit-any\": \"off\"\n}\n}\n]\n}\n```",
99
"type": "object",
1010
"properties": {
11+
"$schema": {
12+
"description": "Schema URI for editor tooling.",
13+
"type": [
14+
"string",
15+
"null"
16+
],
17+
"markdownDescription": "Schema URI for editor tooling."
18+
},
1119
"categories": {
1220
"default": {},
1321
"allOf": [
@@ -136,11 +144,6 @@ expression: json
136144
"$ref": "#/definitions/OxlintSettings"
137145
}
138146
]
139-
},
140-
"$schema": {
141-
"type": "string",
142-
"description": "Schema URI for editor tooling",
143-
"markdownDescription": "Schema URI for editor tooling"
144147
}
145148
},
146149
"additionalProperties": false,

npm/oxlint/configuration_schema.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json\n{\n\"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n\"plugins\": [\"import\", \"typescript\", \"unicorn\"],\n\"env\": {\n\"browser\": true\n},\n\"globals\": {\n\"foo\": \"readonly\"\n},\n\"settings\": {\n},\n\"rules\": {\n\"eqeqeq\": \"warn\",\n\"import/no-cycle\": \"error\",\n\"react/self-closing-comp\": [\"error\", { \"html\": false }]\n},\n\"overrides\": [\n{\n\"files\": [\"*.test.ts\", \"*.spec.ts\"],\n\"rules\": {\n\"@typescript-eslint/no-explicit-any\": \"off\"\n}\n}\n]\n}\n```",
55
"type": "object",
66
"properties": {
7+
"$schema": {
8+
"description": "Schema URI for editor tooling.",
9+
"type": [
10+
"string",
11+
"null"
12+
],
13+
"markdownDescription": "Schema URI for editor tooling."
14+
},
715
"categories": {
816
"default": {},
917
"allOf": [
@@ -132,11 +140,6 @@
132140
"$ref": "#/definitions/OxlintSettings"
133141
}
134142
]
135-
},
136-
"$schema": {
137-
"type": "string",
138-
"description": "Schema URI for editor tooling",
139-
"markdownDescription": "Schema URI for editor tooling"
140143
}
141144
},
142145
"additionalProperties": false,

tasks/website_linter/src/snapshots/schema_markdown.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ Example
6262
```
6363

6464

65+
## $schema
66+
67+
type: `string | null`
68+
69+
70+
Schema URI for editor tooling.
71+
72+
6573
## categories
6674

6775
type: `object`

0 commit comments

Comments
 (0)