Skip to content

Commit b361d99

Browse files
committed
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.
1 parent c229022 commit b361d99

4 files changed

Lines changed: 60 additions & 6 deletions

File tree

rewrite-javascript/rewrite/src/recipe.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,21 +89,30 @@ export abstract class Recipe {
8989

9090
async descriptor(): Promise<RecipeDescriptor> {
9191
const optionsRecord: Record<string, OptionDescriptor> = (this as any).constructor[OPTIONS_KEY] || {}
92+
// Java's `RecipeDescriptor.getXxx()` getters for collection-valued
93+
// fields are treated as never-null by callers (matching what
94+
// `Recipe.getDescriptor()` upholds locally). Always emit the
95+
// collection keys, even when empty, so Jackson on the Java side
96+
// never leaves them null.
9297
return {
9398
name: this.name,
9499
displayName: this.displayName,
95100
instanceName: this.instanceName(),
96101
description: this.description,
97102
tags: this.tags,
98103
estimatedEffortPerOccurrence: this.estimatedEffortPerOccurrence,
99-
recipeList: await mapAsync(await this.recipeList(), async r => r.descriptor()),
100104
options: Object.entries(optionsRecord).map(([key, descriptor]) => ({
101105
name: key,
102106
value: (this as any)[key],
103107
required: descriptor.required ?? true,
104108
...descriptor
105109
})),
106-
dataTables: this.dataTables
110+
preconditions: [],
111+
recipeList: await mapAsync(await this.recipeList(), async r => r.descriptor()),
112+
dataTables: this.dataTables,
113+
maintainers: [],
114+
contributors: [],
115+
examples: []
107116
}
108117
}
109118

@@ -135,9 +144,13 @@ export interface RecipeDescriptor {
135144
readonly description: string
136145
readonly tags: string[]
137146
readonly estimatedEffortPerOccurrence: Minutes
138-
readonly recipeList: RecipeDescriptor[]
139147
readonly options: ({ name: string, value?: any } & OptionDescriptor)[]
148+
readonly preconditions: RecipeDescriptor[]
149+
readonly recipeList: RecipeDescriptor[]
140150
readonly dataTables: DataTableDescriptor[]
151+
readonly maintainers: any[]
152+
readonly contributors: any[]
153+
readonly examples: any[]
141154
}
142155

143156
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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,13 @@ def _category_descriptor_to_dict(descriptor) -> dict:
878878

879879

880880
def _recipe_descriptor_to_dict(descriptor) -> dict:
881-
"""Convert a RecipeDescriptor to a dict for JSON serialization."""
881+
"""Convert a RecipeDescriptor to a dict for JSON serialization.
882+
883+
Java's `RecipeDescriptor.getXxx()` getters for collection-valued fields
884+
are treated as never-null by callers (matching what `Recipe.getDescriptor()`
885+
upholds locally). Always emit the collection keys, even when empty, so
886+
Jackson on the Java side never leaves them null.
887+
"""
882888
return {
883889
'name': descriptor.name,
884890
'displayName': descriptor.display_name,
@@ -897,8 +903,12 @@ def _recipe_descriptor_to_dict(descriptor) -> dict:
897903
}
898904
for name, value, opt in descriptor.options
899905
],
900-
'dataTables': descriptor.data_tables,
906+
'preconditions': [],
901907
'recipeList': [_recipe_descriptor_to_dict(r) for r in descriptor.recipe_list],
908+
'dataTables': descriptor.data_tables,
909+
'maintainers': [],
910+
'contributors': [],
911+
'examples': [],
902912
}
903913

904914

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,30 @@ 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+
"""Java's RecipeDescriptor.getXxx() collection-valued getters are
31+
treated as never-null by callers. Always emit the collection keys —
32+
including the ones the Python dataclass doesn't model — so Jackson on
33+
the Java side never leaves them null."""
34+
from rewrite.recipe import RecipeDescriptor
35+
from rewrite.rpc.server import _recipe_descriptor_to_dict
36+
37+
descriptor = RecipeDescriptor(
38+
name="org.example.Foo",
39+
display_name="Foo",
40+
description="A recipe.",
41+
tags=[],
42+
estimated_effort_per_occurrence=0,
43+
options=[],
44+
data_tables=[],
45+
recipe_list=[],
46+
)
47+
48+
result = _recipe_descriptor_to_dict(descriptor)
49+
50+
for key in ("tags", "options", "preconditions", "recipeList",
51+
"dataTables", "maintainers", "contributors", "examples"):
52+
assert key in result, f"missing key: {key}"
53+
assert result[key] == [], f"{key} should be empty list, got {result[key]!r}"

0 commit comments

Comments
 (0)