Skip to content

Commit 72154fd

Browse files
authored
RPC peers: emit empty arrays for descriptor collections, never omit them (#7546)
* RPC peers: emit empty arrays for descriptor collections, never omit them `Recipe.getDescriptor()` on the Java side always returns a descriptor whose collection-valued getters (`tags`, `options`, `preconditions`, `recipeList`, `dataTables`, `maintainers`, `contributors`, `examples`) are non-null. Callers across the ecosystem rely on that and iterate the getters without null checks. When a `RecipeDescriptor` is produced by a polyglot RPC peer, however, the JSON those peers emit can omit empty collections — Jackson then deserializes the corresponding fields as `null`, breaking every downstream caller (e.g. `descriptor.getPreconditions().iterator()`). Fix the Python and TypeScript peers to always emit every collection key, even when empty. The C# peer was already correct: `RecipeDescriptorDto` declares each list/set property with an `= []` initializer and Newtonsoft serializes empty collections as `[]`, so the JSON it produces already matches the contract. Python (`rewrite/src/rewrite/rpc/server.py`): Add `preconditions`, `maintainers`, `contributors`, `examples` as empty lists in `_recipe_descriptor_to_dict`. (The Python `RecipeDescriptor` dataclass doesn't model these fields yet, so they're hardcoded.) TypeScript (`rewrite/src/recipe.ts`): Extend the `RecipeDescriptor` interface with the four missing fields and have `Recipe.descriptor()` populate them. Updates the existing `recipe.test.ts` snapshot. * Update recipe.ts * Update server.py * Update test_server.py
1 parent b9b436f commit 72154fd

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

rewrite-javascript/rewrite/src/recipe.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,18 @@ export abstract class Recipe {
9696
description: this.description,
9797
tags: this.tags,
9898
estimatedEffortPerOccurrence: this.estimatedEffortPerOccurrence,
99-
recipeList: await mapAsync(await this.recipeList(), async r => r.descriptor()),
10099
options: Object.entries(optionsRecord).map(([key, descriptor]) => ({
101100
name: key,
102101
value: (this as any)[key],
103102
required: descriptor.required ?? true,
104103
...descriptor
105104
})),
106-
dataTables: this.dataTables
105+
preconditions: [],
106+
recipeList: await mapAsync(await this.recipeList(), async r => r.descriptor()),
107+
dataTables: this.dataTables,
108+
maintainers: [],
109+
contributors: [],
110+
examples: []
107111
}
108112
}
109113

@@ -135,9 +139,13 @@ export interface RecipeDescriptor {
135139
readonly description: string
136140
readonly tags: string[]
137141
readonly estimatedEffortPerOccurrence: Minutes
138-
readonly recipeList: RecipeDescriptor[]
139142
readonly options: ({ name: string, value?: any } & OptionDescriptor)[]
143+
readonly preconditions: RecipeDescriptor[]
144+
readonly recipeList: RecipeDescriptor[]
140145
readonly dataTables: DataTableDescriptor[]
146+
readonly maintainers: any[]
147+
readonly contributors: any[]
148+
readonly examples: any[]
141149
}
142150

143151
export interface OptionDescriptor {

rewrite-javascript/rewrite/test/recipe.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ describe("recipes", () => {
4343
value: undefined
4444
}
4545
],
46+
preconditions: [],
4647
recipeList: [],
4748
tags: [],
48-
dataTables: []
49+
dataTables: [],
50+
maintainers: [],
51+
contributors: [],
52+
examples: []
4953
});
5054
});
5155
});

rewrite-python/rewrite/src/rewrite/rpc/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,8 +971,12 @@ def _recipe_descriptor_to_dict(descriptor) -> dict:
971971
}
972972
for name, value, opt in descriptor.options
973973
],
974-
'dataTables': descriptor.data_tables,
974+
'preconditions': [],
975975
'recipeList': [_recipe_descriptor_to_dict(r) for r in descriptor.recipe_list],
976+
'dataTables': descriptor.data_tables,
977+
'maintainers': [],
978+
'contributors': [],
979+
'examples': [],
976980
}
977981

978982

rewrite-python/rewrite/tests/rpc/test_server.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,26 @@ def fake_parse_python_source(source, path="<unknown>", relative_to=None, ty_clie
2424
assert observed["source"] == ""
2525
assert observed["path"] == str(tmp_path / "pkg" / "__init__.py")
2626
assert (tmp_path / "pkg" / "__init__.py").read_text(encoding="utf-8") == ""
27+
28+
29+
def test_recipe_descriptor_to_dict_emits_all_collection_keys():
30+
from rewrite.recipe import RecipeDescriptor
31+
from rewrite.rpc.server import _recipe_descriptor_to_dict
32+
33+
descriptor = RecipeDescriptor(
34+
name="org.example.Foo",
35+
display_name="Foo",
36+
description="A recipe.",
37+
tags=[],
38+
estimated_effort_per_occurrence=0,
39+
options=[],
40+
data_tables=[],
41+
recipe_list=[],
42+
)
43+
44+
result = _recipe_descriptor_to_dict(descriptor)
45+
46+
for key in ("tags", "options", "preconditions", "recipeList",
47+
"dataTables", "maintainers", "contributors", "examples"):
48+
assert key in result, f"missing key: {key}"
49+
assert result[key] == [], f"{key} should be empty list, got {result[key]!r}"

0 commit comments

Comments
 (0)