Warn about unknown fields when editing settings json (#33678)
Closes #30017 * While generating the settings JSON schema, defaults all schema definitions to reject unknown fields via `additionalProperties: false`. * Uses `unevaluatedProperties: false` at the top level to check fields that remain after the settings field names + release stage override field names. * Changes json schema version from `draft07` to `draft_2019_09` to have support for `unevaluatedProperties`. Release Notes: - Added warnings for unknown fields when editing `settings.json`.
This commit is contained in:
parent
7609ca7a8d
commit
c74ecb4654
14 changed files with 86 additions and 40 deletions
|
@ -427,7 +427,7 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
|
||||
let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
|
||||
let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
|
||||
|
||||
let action_schemas = cx.action_schemas(&mut generator);
|
||||
let deprecations = cx.deprecated_actions_to_preferred_actions();
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::{JsonSchema, Schema};
|
||||
use schemars::{JsonSchema, Schema, transform::transform_subschemas};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
use std::{ops::Range, sync::LazyLock};
|
||||
use tree_sitter::{Query, StreamingIterator as _};
|
||||
use util::RangeExt;
|
||||
|
||||
/// Parameters that are used when generating some JSON schemas at runtime.
|
||||
pub struct SettingsJsonSchemaParams<'a> {
|
||||
pub language_names: &'a [String],
|
||||
pub font_names: &'a [String],
|
||||
}
|
||||
|
||||
/// Value registered which specifies JSON schemas that are generated at runtime.
|
||||
pub struct ParameterizedJsonSchema {
|
||||
pub add_and_get_ref:
|
||||
fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
|
||||
|
@ -19,24 +21,26 @@ pub struct ParameterizedJsonSchema {
|
|||
|
||||
inventory::collect!(ParameterizedJsonSchema);
|
||||
|
||||
const DEFS_PATH: &str = "#/$defs/";
|
||||
|
||||
/// Replaces the JSON schema definition for some type, and returns a reference to it.
|
||||
pub fn replace_subschema<T: JsonSchema>(
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
schema: schemars::Schema,
|
||||
) -> schemars::Schema {
|
||||
const DEFINITIONS_PATH: &str = "#/definitions/";
|
||||
// The key in definitions may not match T::schema_name() if multiple types have the same name.
|
||||
// This is a workaround for there being no straightforward way to get the key used for a type -
|
||||
// see https://github.com/GREsau/schemars/issues/449
|
||||
let ref_schema = generator.subschema_for::<T>();
|
||||
if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") {
|
||||
if let Some(definition_name) = definition_pointer.strip_prefix(DEFINITIONS_PATH) {
|
||||
if let Some(definition_name) = definition_pointer.strip_prefix(DEFS_PATH) {
|
||||
generator
|
||||
.definitions_mut()
|
||||
.insert(definition_name.to_string(), schema.to_value());
|
||||
return ref_schema;
|
||||
} else {
|
||||
log::error!(
|
||||
"bug: expected `$ref` field to start with {DEFINITIONS_PATH}, \
|
||||
"bug: expected `$ref` field to start with {DEFS_PATH}, \
|
||||
got {definition_pointer}"
|
||||
);
|
||||
}
|
||||
|
@ -48,7 +52,39 @@ pub fn replace_subschema<T: JsonSchema>(
|
|||
generator
|
||||
.definitions_mut()
|
||||
.insert(schema_name.to_string(), schema.to_value());
|
||||
Schema::new_ref(format!("{DEFINITIONS_PATH}{schema_name}"))
|
||||
Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
|
||||
}
|
||||
|
||||
/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
|
||||
/// already in use.
|
||||
pub fn add_new_subschema(
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
name: &str,
|
||||
schema: Value,
|
||||
) -> Schema {
|
||||
let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
|
||||
assert_eq!(old_definition, None);
|
||||
schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
|
||||
}
|
||||
|
||||
/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
|
||||
/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
|
||||
/// on a map).
|
||||
#[derive(Clone)]
|
||||
pub struct DefaultDenyUnknownFields;
|
||||
|
||||
impl schemars::transform::Transform for DefaultDenyUnknownFields {
|
||||
fn transform(&mut self, schema: &mut schemars::Schema) {
|
||||
if let Some(object) = schema.as_object_mut() {
|
||||
if object.contains_key("properties")
|
||||
&& !object.contains_key("additionalProperties")
|
||||
&& !object.contains_key("unevaluatedProperties")
|
||||
{
|
||||
object.insert("additionalProperties".to_string(), false.into());
|
||||
}
|
||||
}
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_value_in_json_text<'a>(
|
||||
|
|
|
@ -24,8 +24,8 @@ use util::{ResultExt as _, merge_non_null_json_value_into};
|
|||
pub type EditorconfigProperties = ec4rs::Properties;
|
||||
|
||||
use crate::{
|
||||
ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
|
||||
parse_json_with_comments, update_value_in_json_text,
|
||||
DefaultDenyUnknownFields, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
|
||||
WorktreeId, add_new_subschema, parse_json_with_comments, update_value_in_json_text,
|
||||
};
|
||||
|
||||
/// A value that can be defined as a user setting.
|
||||
|
@ -864,7 +864,9 @@ impl SettingsStore {
|
|||
}
|
||||
|
||||
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value {
|
||||
let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
|
||||
let mut generator = schemars::generate::SchemaSettings::draft2019_09()
|
||||
.with_transform(DefaultDenyUnknownFields)
|
||||
.into_generator();
|
||||
let mut combined_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
|
@ -988,16 +990,34 @@ impl SettingsStore {
|
|||
}
|
||||
}
|
||||
|
||||
// add schemas which are determined at runtime
|
||||
for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
|
||||
(parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
|
||||
}
|
||||
|
||||
// add merged settings schema to the definitions
|
||||
const ZED_SETTINGS: &str = "ZedSettings";
|
||||
let old_zed_settings_definition = generator
|
||||
.definitions_mut()
|
||||
.insert(ZED_SETTINGS.to_string(), combined_schema);
|
||||
assert_eq!(old_zed_settings_definition, None);
|
||||
let zed_settings_ref = schemars::Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}"));
|
||||
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
|
||||
|
||||
// add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
|
||||
// fields are rejected.
|
||||
let mut zed_release_stage_settings = zed_settings_ref.clone();
|
||||
zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
|
||||
let zed_release_stage_settings_ref = add_new_subschema(
|
||||
&mut generator,
|
||||
"ZedReleaseStageSettings",
|
||||
zed_release_stage_settings.to_value(),
|
||||
);
|
||||
|
||||
// Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
|
||||
// unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
|
||||
let mut definitions = generator.take_definitions(true);
|
||||
definitions
|
||||
.get_mut(ZED_SETTINGS)
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("additionalProperties");
|
||||
|
||||
let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() {
|
||||
json_schema!({ "$schema": meta_schema.to_string() })
|
||||
|
@ -1005,25 +1025,26 @@ impl SettingsStore {
|
|||
json_schema!({})
|
||||
};
|
||||
|
||||
// settings file contents matches ZedSettings + overrides for each release stage
|
||||
// "unevaluatedProperties: false" to report unknown fields.
|
||||
root_schema.insert("unevaluatedProperties".to_string(), false.into());
|
||||
|
||||
// Settings file contents matches ZedSettings + overrides for each release stage.
|
||||
root_schema.insert(
|
||||
"allOf".to_string(),
|
||||
json!([
|
||||
zed_settings_ref,
|
||||
{
|
||||
"properties": {
|
||||
"dev": zed_settings_ref,
|
||||
"nightly": zed_settings_ref,
|
||||
"stable": zed_settings_ref,
|
||||
"preview": zed_settings_ref,
|
||||
"dev": zed_release_stage_settings_ref,
|
||||
"nightly": zed_release_stage_settings_ref,
|
||||
"stable": zed_release_stage_settings_ref,
|
||||
"preview": zed_release_stage_settings_ref,
|
||||
}
|
||||
}
|
||||
]),
|
||||
);
|
||||
root_schema.insert(
|
||||
"definitions".to_string(),
|
||||
generator.take_definitions(true).into(),
|
||||
);
|
||||
|
||||
root_schema.insert("$defs".to_string(), definitions.into());
|
||||
|
||||
root_schema.to_value()
|
||||
}
|
||||
|
@ -1934,7 +1955,6 @@ mod tests {
|
|||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
struct UserSettingsContent {
|
||||
name: Option<String>,
|
||||
age: Option<u32>,
|
||||
|
@ -1977,7 +1997,6 @@ mod tests {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
struct MultiKeySettingsJson {
|
||||
key1: Option<String>,
|
||||
key2: Option<String>,
|
||||
|
@ -2016,7 +2035,6 @@ mod tests {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
struct JournalSettingsJson {
|
||||
pub path: Option<String>,
|
||||
pub hour_format: Option<HourFormat>,
|
||||
|
@ -2111,7 +2129,6 @@ mod tests {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
struct LanguageSettingEntry {
|
||||
language_setting_1: Option<bool>,
|
||||
language_setting_2: Option<bool>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue