Fix code actions migration (#40303)

Closes #40270

Release Notes:

- Fixed an issue with the settings migration to flatten `code_actions`
format steps where comments would cause enabled code actions to be
omitted from the migrated settings. If you were effected, restoring the
settings file backup and allowing the migration to re-run will result in
a valid settings file
- Fixed an issue where automated settings and keymap file updates would
occasionally assume 4-space indentation
This commit is contained in:
Ben Kunkle
2025-10-16 09:13:34 -04:00
committed by GitHub
parent c37a2f885a
commit ea6853d35c
10 changed files with 786 additions and 538 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ pub(crate) mod m_2025_07_08 {
pub(crate) mod m_2025_10_01 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
pub(crate) use settings::flatten_code_actions_formatters;
}
pub(crate) mod m_2025_10_02 {
@@ -1,109 +1,74 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::patterns::migrate_language_setting;
use anyhow::Result;
use serde_json::Value;
use crate::MigrationPatterns;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(FORMATTER_PATTERN, migrate_code_action_formatters)];
const FORMATTER_PATTERN: &str = r#"
(object
(pair
key: (string (string_content) @formatter) (#any-of? @formatter "formatter" "format_on_save")
value: [
(array
(object
(pair
key: (string (string_content) @code-actions-key) (#eq? @code-actions-key "code_actions")
value: (object
((pair) @code-action ","?)*
)
)
) @code-actions-obj
) @formatter-array
(object
(pair
key: (string (string_content) @code-actions-key) (#eq? @code-actions-key "code_actions")
value: (object
((pair) @code-action ","?)*
)
)
) @code-actions-obj
]
)
)
"#;
pub fn migrate_code_action_formatters(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let code_actions_obj_ix = query.capture_index_for_name("code-actions-obj")?;
let code_actions_obj_node = mat.nodes_for_capture_index(code_actions_obj_ix).next()?;
let mut code_actions = vec![];
let code_actions_ix = query.capture_index_for_name("code-action")?;
for code_action_node in mat.nodes_for_capture_index(code_actions_ix) {
let Some(enabled) = code_action_node
.child_by_field_name("value")
.map(|n| n.kind() != "false")
else {
continue;
pub fn flatten_code_actions_formatters(value: &mut Value) -> Result<()> {
migrate_language_setting(value, |value, _path| {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
if !enabled {
continue;
}
let Some(name) = code_action_node
.child_by_field_name("key")
.and_then(|n| n.child(1))
.map(|n| &contents[n.byte_range()])
else {
continue;
};
code_actions.push(name);
}
for key in ["formatter", "format_on_save"] {
let Some(formatter) = obj.get_mut(key) else {
continue;
};
let new_formatter = match formatter {
Value::Array(arr) => {
let mut new_arr = Vec::new();
let mut found_code_actions = false;
for item in arr {
let Some(obj) = item.as_object() else {
new_arr.push(item.clone());
continue;
};
let code_actions_obj = obj
.get("code_actions")
.and_then(|code_actions| code_actions.as_object());
let Some(code_actions) = code_actions_obj else {
new_arr.push(item.clone());
continue;
};
found_code_actions = true;
for (name, enabled) in code_actions {
if !enabled.as_bool().unwrap_or(true) {
continue;
}
new_arr.push(serde_json::json!({
"code_action": name
}));
}
}
if !found_code_actions {
continue;
}
Value::Array(new_arr)
}
Value::Object(obj) => {
let mut new_arr = Vec::new();
let code_actions_obj = obj
.get("code_actions")
.and_then(|code_actions| code_actions.as_object());
let Some(code_actions) = code_actions_obj else {
continue;
};
for (name, enabled) in code_actions {
if !enabled.as_bool().unwrap_or(true) {
continue;
}
new_arr.push(serde_json::json!({
"code_action": name
}));
}
if new_arr.len() == 1 {
new_arr.pop().unwrap()
} else {
Value::Array(new_arr)
}
}
_ => continue,
};
let indent = query
.capture_index_for_name("formatter")
.and_then(|ix| mat.nodes_for_capture_index(ix).next())
.map(|node| node.start_position().column + 1)
.unwrap_or(2);
let mut code_actions_str = code_actions
.into_iter()
.map(|code_action| format!(r#"{{ "code_action": "{}" }}"#, code_action))
.collect::<Vec<_>>()
.join(&format!(",\n{}", " ".repeat(indent)));
let is_array = query
.capture_index_for_name("formatter-array")
.map(|ix| mat.nodes_for_capture_index(ix).count() > 0)
.unwrap_or(false);
if !is_array {
code_actions_str.insert_str(0, &" ".repeat(indent));
code_actions_str.insert_str(0, "[\n");
code_actions_str.push('\n');
code_actions_str.push_str(&" ".repeat(indent.saturating_sub(2)));
code_actions_str.push_str("]");
}
let mut replace_range = code_actions_obj_node.byte_range();
if is_array && code_actions_str.is_empty() {
let mut cursor = code_actions_obj_node.parent().unwrap().walk();
cursor.goto_first_child();
while cursor.node().id() != code_actions_obj_node.id() && cursor.goto_next_sibling() {}
while cursor.goto_next_sibling()
&& (cursor.node().is_extra()
|| cursor.node().is_missing()
|| cursor.node().kind() == "comment")
{}
if cursor.node().kind() == "," {
// found comma, delete up to next node
while cursor.goto_next_sibling()
&& (cursor.node().is_extra() || cursor.node().is_missing())
{}
replace_range.end = cursor.node().range().start_byte;
obj.insert(key.to_string(), new_formatter);
}
}
Some((replace_range, code_actions_str))
return Ok(());
})
}
@@ -1,19 +1,10 @@
use anyhow::Result;
use serde_json::Value;
use crate::patterns::migrate_language_setting;
pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> {
remove_formatters_on_save_inner(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
remove_formatters_on_save_inner(language, &path)?;
}
}
Ok(())
migrate_language_setting(value, remove_formatters_on_save_inner)
}
fn remove_formatters_on_save_inner(value: &mut Value, path: &[&str]) -> Result<()> {
@@ -1,19 +1,10 @@
use anyhow::Result;
use serde_json::Value;
use crate::patterns::migrate_language_setting;
pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
remove_code_actions_on_format_inner(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
remove_code_actions_on_format_inner(language, &path)?;
}
}
Ok(())
migrate_language_setting(value, remove_code_actions_on_format_inner)
}
fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
+498 -375
View File
@@ -74,6 +74,7 @@ fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<Str
let mut current_text = text.to_string();
let mut result: Option<String> = None;
let json_indent_size = settings::infer_json_indent_size(&current_text);
for migration in migrations.iter() {
let migrated_text = match migration {
MigrationType::TreeSitter(patterns, query) => migrate(&current_text, patterns, query)?,
@@ -92,7 +93,7 @@ fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<Str
settings::update_value_in_json_text(
&mut current,
&mut vec![],
2,
json_indent_size,
&old_value,
&new_value,
&mut edits,
@@ -204,10 +205,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_07_08::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_07_08,
),
MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
),
MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
MigrationType::TreeSitter(
migrations::m_2025_10_03::SETTINGS_PATTERNS,
@@ -328,10 +326,6 @@ define_query!(
SETTINGS_QUERY_2025_07_08,
migrations::m_2025_07_08::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_10_01,
migrations::m_2025_10_01::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_10_03,
migrations::m_2025_10_03::SETTINGS_PATTERNS
@@ -351,10 +345,11 @@ mod tests {
use super::*;
use unindent::Unindent as _;
#[track_caller]
fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
match (&migrated, &expected) {
(Some(migrated), Some(expected)) => {
pretty_assertions::assert_str_eq!(migrated, expected);
pretty_assertions::assert_str_eq!(expected, migrated);
}
_ => {
pretty_assertions::assert_eq!(migrated.as_deref(), expected);
@@ -372,6 +367,7 @@ mod tests {
assert_migrated_correctly(migrated, output);
}
#[track_caller]
fn assert_migrate_settings_with_migrations(
migrations: &[MigrationType],
input: &str,
@@ -1343,24 +1339,28 @@ mod tests {
fn test_flatten_code_action_formatters_basic_array() {
assert_migrate_settings(
&r#"{
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
}
]
}"#
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
}
]
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" }
]
}"#
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
}
]
}"#
.unindent(),
),
);
@@ -1370,21 +1370,25 @@ mod tests {
fn test_flatten_code_action_formatters_basic_object() {
assert_migrate_settings(
&r#"{
"formatter": {
"code_actions": {
"included-1": true,
"excluded": false,
"included-2": true
}
}
}"#
"formatter": {
"code_actions": {
"included-1": true,
"excluded": false,
"included-2": true
}
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" }
]
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
}
]
}"#
.unindent(),
),
@@ -1394,47 +1398,57 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
assert_migrate_settings(
r#"{
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}"#,
&r#"{
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}"#
.unindent(),
Some(
r#"{
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" },
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
}"#,
&r#"{
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
},
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
}"#
.unindent(),
),
);
}
@@ -1443,55 +1457,63 @@ mod tests {
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
assert_migrate_settings(
&r#"{
"languages": {
"Rust": {
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
"languages": {
"Rust": {
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
}"#
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
}
}"#
.unindent(),
Some(
&r#"{
"languages": {
"Rust": {
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" },
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
}
}
}"#
"languages": {
"Rust": {
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
},
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
}
}
}"#
.unindent(),
),
);
@@ -1502,100 +1524,120 @@ mod tests {
{
assert_migrate_settings(
&r#"{
"formatter": {
"code_actions": {
"default-1": true,
"default-2": true,
"default-3": true,
"default-4": true,
}
},
"languages": {
"Rust": {
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
},
"Python": {
"formatter": [
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
"formatter": {
"code_actions": {
"default-1": true,
"default-2": true,
"default-3": true,
"default-4": true,
}
},
"languages": {
"Rust": {
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
}"#
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
},
"Python": {
"formatter": [
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{ "code_action": "default-1" },
{ "code_action": "default-2" },
{ "code_action": "default-3" },
{ "code_action": "default-4" }
],
"languages": {
"Rust": {
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" },
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
},
"Python": {
"formatter": [
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
}
}
}"#
"formatter": [
{
"code_action": "default-1"
},
{
"code_action": "default-2"
},
{
"code_action": "default-3"
},
{
"code_action": "default-4"
}
],
"languages": {
"Rust": {
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
},
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
},
"Python": {
"formatter": [
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
}
}
}"#
.unindent(),
),
);
@@ -1604,153 +1646,185 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
&[MigrationType::Json(
migrations::m_2025_10_01::flatten_code_actions_formatters,
)],
&r#"{
"formatter": {
"code_actions": {
"default-1": true,
"default-2": true,
"default-3": true,
"default-4": true,
}
},
"format_on_save": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
],
"languages": {
"Rust": {
"format_on_save": "prettier",
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
},
"Python": {
"format_on_save": {
"code_actions": {
"on-save-1": true,
"on-save-2": true,
}
},
"formatter": [
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
"formatter": {
"code_actions": {
"default-1": true,
"default-2": true,
"default-3": true,
"default-4": true,
}
},
"format_on_save": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
],
"languages": {
"Rust": {
"format_on_save": "prettier",
"formatter": [
{
"code_actions": {
"included-1": true,
"included-2": true,
"excluded": false,
}
}"#
},
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
},
"Python": {
"format_on_save": {
"code_actions": {
"on-save-1": true,
"on-save-2": true,
}
},
"formatter": [
{
"language_server": "ruff"
},
{
"code_actions": {
"excluded": false,
"excluded-2": false,
}
}
// some comment
,
{
"code_actions": {
"excluded": false,
"included-3": true,
"included-4": true,
}
},
]
}
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{ "code_action": "default-1" },
{ "code_action": "default-2" },
{ "code_action": "default-3" },
{ "code_action": "default-4" }
],
"format_on_save": [
{ "code_action": "included-1" },
{ "code_action": "included-2" },
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
],
"languages": {
"Rust": {
"format_on_save": "prettier",
"formatter": [
{ "code_action": "included-1" },
{ "code_action": "included-2" },
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
},
"Python": {
"format_on_save": [
{ "code_action": "on-save-1" },
{ "code_action": "on-save-2" }
],
"formatter": [
{
"language_server": "ruff"
},
{ "code_action": "included-3" },
{ "code_action": "included-4" },
]
}
}
}"#
&r#"
{
"formatter": [
{
"code_action": "default-1"
},
{
"code_action": "default-2"
},
{
"code_action": "default-3"
},
{
"code_action": "default-4"
}
],
"format_on_save": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
},
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
],
"languages": {
"Rust": {
"format_on_save": "prettier",
"formatter": [
{
"code_action": "included-1"
},
{
"code_action": "included-2"
},
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
},
"Python": {
"format_on_save": [
{
"code_action": "on-save-1"
},
{
"code_action": "on-save-2"
}
],
"formatter": [
{
"language_server": "ruff"
},
{
"code_action": "included-3"
},
{
"code_action": "included-4"
}
]
}
}
}"#
.unindent(),
),
);
@@ -1952,25 +2026,25 @@ mod tests {
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"a": true,
"b": false,
"c": true
}
}"#
"code_actions_on_format": {
"a": true,
"b": false,
"c": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "a"
},
{
"code_action": "c"
}
]
}
"#
"formatter": [
{
"code_action": "a"
},
{
"code_action": "c"
}
]
}
"#
.unindent(),
),
);
@@ -2163,12 +2237,12 @@ mod tests {
]
},
"Python": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
}"#
.unindent(),
@@ -2212,4 +2286,53 @@ mod tests {
),
);
}
#[test]
fn test_code_action_formatters_issue() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_01::flatten_code_actions_formatters,
)],
&r#"
{
"languages": {
"Python": {
"language_servers": ["ruff"],
"format_on_save": "on",
"formatter": [
{
"code_actions": {
// Fix all auto-fixable lint violations
"source.fixAll.ruff": true,
// Organize imports
"source.organizeImports.ruff": true
}
}
]
}
}
}"#
.unindent(),
Some(
&r#"
{
"languages": {
"Python": {
"language_servers": ["ruff"],
"format_on_save": "on",
"formatter": [
{
"code_action": "source.fixAll.ruff"
},
{
"code_action": "source.organizeImports.ruff"
}
]
}
}
}"#
.unindent(),
),
);
}
}
+1
View File
@@ -10,4 +10,5 @@ pub(crate) use settings::{
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
migrate_language_setting,
};
+21
View File
@@ -108,3 +108,24 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
(#eq? @agent1 "agent")
(#eq? @agent2 "agent")
)"#;
/// Migrate language settings,
/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
/// Fails early if `migrate_fn` returns an error at any point
pub fn migrate_language_setting(
value: &mut serde_json::Value,
migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
migrate_fn(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
migrate_fn(language, &path)?;
}
}
Ok(())
}