Skip to content

Commit 73a1693

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: java2spec extracts for-loop patterns as repeated fields and recognizes writeStringList
1 parent 7962a85 commit 73a1693

5 files changed

Lines changed: 382 additions & 44 deletions

File tree

tools/cmd/java2spec/main.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,7 @@ func mergeJavaWireFormats(
254254
continue
255255
}
256256

257-
wireFields := make([]spec.JavaWireField, len(js.Fields))
258-
for i, f := range js.Fields {
259-
wireFields[i] = spec.JavaWireField{
260-
Name: f.Name,
261-
WriteMethod: f.Type,
262-
Condition: f.Condition,
263-
DelegateType: f.DelegateType,
264-
}
265-
}
257+
wireFields := convertFieldSpecs(js.Fields)
266258

267259
mu.Lock()
268260
// Prefer the wire format with more fields — avoids
@@ -289,6 +281,22 @@ func mergeJavaWireFormats(
289281
return nil
290282
}
291283

284+
// convertFieldSpecs converts parcelspec.FieldSpec entries to spec.JavaWireField,
285+
// recursively converting Elements for repeated (loop) fields.
286+
func convertFieldSpecs(fields []parcelspec.FieldSpec) []spec.JavaWireField {
287+
result := make([]spec.JavaWireField, len(fields))
288+
for i, f := range fields {
289+
result[i] = spec.JavaWireField{
290+
Name: f.Name,
291+
WriteMethod: f.Type,
292+
Condition: f.Condition,
293+
DelegateType: f.DelegateType,
294+
Elements: convertFieldSpecs(f.Elements),
295+
}
296+
}
297+
return result
298+
}
299+
292300
// constantSpec describes one set of Java constants to extract.
293301
type constantSpec struct {
294302
JavaFile string `yaml:"java_file"`

tools/pkg/parcelspec/field_spec.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package parcelspec
22

33
// FieldSpec describes a single field in a Parcelable wire format.
44
type FieldSpec struct {
5-
Name string `yaml:"name"`
6-
Type string `yaml:"type"` // bool, int32, int64, float32, float64, string8, string16, opaque
7-
Condition string `yaml:"condition,omitempty"` // e.g. "FieldsMask & 1"
8-
DelegateType string `yaml:"delegate_type,omitempty"` // helper class for static delegates (e.g. "TextUtils")
5+
Name string `yaml:"name"`
6+
Type string `yaml:"type"` // bool, int32, int64, float32, float64, string8, string16, repeated, opaque
7+
Condition string `yaml:"condition,omitempty"` // e.g. "FieldsMask & 1"
8+
DelegateType string `yaml:"delegate_type,omitempty"` // helper class for static delegates (e.g. "TextUtils")
9+
Elements []FieldSpec `yaml:"elements,omitempty"` // sub-fields for repeated (loop) fields
910
}

tools/pkg/parcelspec/java_extractor.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package parcelspec
22

33
import (
4+
"strings"
45
"unicode"
56

67
antlr "github.com/antlr4-go/antlr/v4"
@@ -65,35 +66,44 @@ func ExtractSpecs(
6566
// javaWriteMethodToSpecType maps Java Parcel write method names
6667
// to their corresponding spec type strings.
6768
var javaWriteMethodToSpecType = map[string]string{
68-
"writeString8": "string8",
69-
"writeString": "string16",
70-
"writeString16": "string16",
71-
"writeInt": "int32",
72-
"writeLong": "int64",
73-
"writeFloat": "float32",
74-
"writeDouble": "float64",
75-
"writeBoolean": "bool",
76-
"writeBundle": "bundle",
77-
"writeParcelable": "typed_object",
78-
"writeTypedObject": "typed_object",
79-
"writeByte": "int32",
80-
"writeByteArray": "byte_array",
81-
"writeIntArray": "int_array",
82-
"writeLongArray": "long_array",
83-
"writeFloatArray": "float_array",
84-
"writeDoubleArray": "double_array",
85-
"writeBooleanArray": "boolean_array",
86-
"writeStringArray": "string_array",
87-
"writeBlob": "blob",
88-
"writeStrongBinder": "binder",
89-
"writeList": "write_list",
90-
"writeTypedArray": "typed_array",
91-
"writeArraySet": "array_set",
69+
"writeString8": "string8",
70+
"writeString": "string16",
71+
"writeString16": "string16",
72+
"writeInt": "int32",
73+
"writeLong": "int64",
74+
"writeFloat": "float32",
75+
"writeDouble": "float64",
76+
"writeBoolean": "bool",
77+
"writeBundle": "bundle",
78+
"writeParcelable": "typed_object",
79+
"writeTypedObject": "typed_object",
80+
"writeByte": "int32",
81+
"writeByteArray": "byte_array",
82+
"writeIntArray": "int_array",
83+
"writeLongArray": "long_array",
84+
"writeFloatArray": "float_array",
85+
"writeDoubleArray": "double_array",
86+
"writeBooleanArray": "boolean_array",
87+
"writeStringArray": "string_array",
88+
"writeBlob": "blob",
89+
"writeStrongBinder": "binder",
90+
"writeList": "write_list",
91+
"writeTypedArray": "typed_array",
92+
"writeArraySet": "array_set",
93+
"writeStringList": "string_list",
94+
"writePersistableBundle": "persistable_bundle",
9295
}
9396

9497
// deriveFieldName converts a Java field name to a spec field name
9598
// by stripping the leading "m" prefix convention.
99+
// For dot-qualified names (e.g., "item.mText"), only the last segment
100+
// is used, so the loop variable prefix is discarded.
96101
func deriveFieldName(javaFieldName string) string {
102+
// Strip qualifier prefix: "item.mText" -> "mText".
103+
if idx := strings.LastIndexByte(javaFieldName, '.'); idx >= 0 {
104+
javaFieldName = javaFieldName[idx+1:]
105+
}
106+
97107
if len(javaFieldName) < 2 {
98108
return javaFieldName
99109
}

tools/pkg/parcelspec/java_extractor_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,145 @@ func TestExtractSpec_Location(t *testing.T) {
250250
require.Empty(t, lastField.Condition)
251251
}
252252

253+
func TestExtractSpecs_WriteStringList(t *testing.T) {
254+
src := `
255+
package com.example;
256+
257+
public class Desc implements Parcelable {
258+
@Override
259+
public void writeToParcel(Parcel dest, int flags) {
260+
dest.writeStringList(mMimeTypes);
261+
dest.writeLong(mTimeStamp);
262+
}
263+
}
264+
`
265+
266+
specs := ExtractSpecs(src, "com.example")
267+
require.Len(t, specs, 1)
268+
269+
fields := specs[0].Fields
270+
require.Len(t, fields, 2)
271+
272+
require.Equal(t, "MimeTypes", fields[0].Name)
273+
require.Equal(t, "string_list", fields[0].Type)
274+
275+
require.Equal(t, "TimeStamp", fields[1].Name)
276+
require.Equal(t, "int64", fields[1].Type)
277+
}
278+
279+
func TestExtractSpecs_ForLoop(t *testing.T) {
280+
src := `
281+
package com.example;
282+
283+
public class Container implements Parcelable {
284+
private int mFlags;
285+
private ArrayList<Item> mItems;
286+
287+
@Override
288+
public void writeToParcel(Parcel dest, int flags) {
289+
dest.writeInt(mFlags);
290+
final int N = mItems.size();
291+
dest.writeInt(N);
292+
for (int i=0; i<N; i++) {
293+
Item item = mItems.get(i);
294+
dest.writeString8(item.mName);
295+
dest.writeInt(item.mValue);
296+
}
297+
}
298+
}
299+
`
300+
301+
specs := ExtractSpecs(src, "com.example")
302+
require.Len(t, specs, 1)
303+
304+
fields := specs[0].Fields
305+
require.Len(t, fields, 2, "expected mFlags + repeated Items")
306+
307+
require.Equal(t, "Flags", fields[0].Name)
308+
require.Equal(t, "int32", fields[0].Type)
309+
310+
require.Equal(t, "Items", fields[1].Name)
311+
require.Equal(t, "repeated", fields[1].Type)
312+
require.Len(t, fields[1].Elements, 2)
313+
require.Equal(t, "Name", fields[1].Elements[0].Name)
314+
require.Equal(t, "string8", fields[1].Elements[0].Type)
315+
require.Equal(t, "Value", fields[1].Elements[1].Name)
316+
require.Equal(t, "int32", fields[1].Elements[1].Type)
317+
}
318+
319+
func TestExtractSpec_ClipDescription(t *testing.T) {
320+
src, err := os.ReadFile(
321+
"../3rdparty/frameworks-base/core/java/android/content/ClipDescription.java",
322+
)
323+
if err != nil {
324+
t.Skip("3rdparty submodules not available")
325+
}
326+
327+
specs := ExtractSpecs(string(src), "android.content")
328+
require.Len(t, specs, 1)
329+
330+
spec := specs[0]
331+
require.Equal(t, "ClipDescription", spec.Type)
332+
333+
// Find MimeTypes field -- should be string_list, not opaque.
334+
var mimeField *FieldSpec
335+
for i := range spec.Fields {
336+
if spec.Fields[i].Name == "MimeTypes" {
337+
mimeField = &spec.Fields[i]
338+
break
339+
}
340+
}
341+
require.NotNil(t, mimeField, "MimeTypes field must exist")
342+
require.Equal(t, "string_list", mimeField.Type)
343+
}
344+
345+
func TestExtractSpec_ClipData(t *testing.T) {
346+
src, err := os.ReadFile(
347+
"../3rdparty/frameworks-base/core/java/android/content/ClipData.java",
348+
)
349+
if err != nil {
350+
t.Skip("3rdparty submodules not available")
351+
}
352+
353+
specs := ExtractSpecs(string(src), "android.content")
354+
require.NotEmpty(t, specs)
355+
356+
// Find the ClipData spec (might have inner classes too).
357+
var clipSpec *ParcelableSpec
358+
for i := range specs {
359+
if specs[i].Type == "ClipData" {
360+
clipSpec = &specs[i]
361+
break
362+
}
363+
}
364+
require.NotNil(t, clipSpec, "ClipData spec must exist")
365+
366+
// Find the repeated Items field.
367+
var itemsField *FieldSpec
368+
for i := range clipSpec.Fields {
369+
if clipSpec.Fields[i].Name == "Items" {
370+
itemsField = &clipSpec.Fields[i]
371+
break
372+
}
373+
}
374+
require.NotNil(t, itemsField, "Items repeated field must exist")
375+
require.Equal(t, "repeated", itemsField.Type)
376+
require.NotEmpty(t, itemsField.Elements, "Items must have element fields")
377+
378+
// The first element should be the Text char_sequence.
379+
require.Equal(t, "Text", itemsField.Elements[0].Name)
380+
require.Equal(t, "char_sequence", itemsField.Elements[0].Type)
381+
382+
// HtmlText is the second element.
383+
require.Equal(t, "HtmlText", itemsField.Elements[1].Name)
384+
require.Equal(t, "string8", itemsField.Elements[1].Type)
385+
386+
// There should be no standalone "N" int32 field (it was subsumed by repeated).
387+
for _, f := range clipSpec.Fields {
388+
require.NotEqual(t, "N", f.Name, "count field N should be subsumed by repeated")
389+
}
390+
}
391+
253392
func TestDeriveFieldName(t *testing.T) {
254393
tests := []struct {
255394
input string
@@ -261,6 +400,8 @@ func TestDeriveFieldName(t *testing.T) {
261400
{"provider", "Provider"},
262401
{"x", "x"},
263402
{"mX", "X"},
403+
{"item.mText", "Text"},
404+
{"item.mHtmlText", "HtmlText"},
264405
}
265406

266407
for _, tc := range tests {

0 commit comments

Comments
 (0)