Skip to content

Commit 7489d27

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat(spec): add MaxAPILevel to ParamSpec for cross-version param tracking
1 parent 5a8d635 commit 7489d27

4 files changed

Lines changed: 355 additions & 13 deletions

File tree

tools/cmd/aidl2spec/version_diff.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,81 @@ import (
1111
)
1212

1313
// diffMethodParams compares oldMethod (from oldAPI) with newMethod (from newAPI)
14-
// and sets MinAPILevel on params that were added in newAPI.
15-
// Only trailing param additions are detected (AIDL stability guarantees
16-
// params are never removed or reordered).
14+
// and annotates params with MinAPILevel / MaxAPILevel to record cross-version
15+
// differences. The comparison walks params by position:
16+
//
17+
// - Same type at position N → kept as-is (present in both versions).
18+
// - Different type at position N → old param gets MaxAPILevel=oldAPI,
19+
// new param gets MinAPILevel=newAPI. Both appear in the result.
20+
// - Trailing params only in newMethod → MinAPILevel=newAPI.
21+
// - Trailing params only in oldMethod → MaxAPILevel=oldAPI.
1722
func diffMethodParams(
1823
oldMethod spec.MethodSpec,
1924
newMethod spec.MethodSpec,
2025
oldAPI int,
2126
newAPI int,
2227
) []spec.ParamSpec {
23-
result := make([]spec.ParamSpec, len(newMethod.Params))
24-
copy(result, newMethod.Params)
28+
oldParams := oldMethod.Params
29+
newParams := newMethod.Params
2530

26-
oldCount := len(oldMethod.Params)
27-
for i := range result {
28-
if i >= oldCount {
29-
result[i].MinAPILevel = newAPI
31+
minLen := len(oldParams)
32+
if len(newParams) < minLen {
33+
minLen = len(newParams)
34+
}
35+
36+
var result []spec.ParamSpec
37+
38+
// Walk positions present in both versions.
39+
for i := 0; i < minLen; i++ {
40+
if oldParams[i].Type.Equal(newParams[i].Type) {
41+
result = append(result, newParams[i])
42+
continue
3043
}
44+
45+
// Type changed at this position: emit both variants with version bounds.
46+
old := oldParams[i]
47+
old.MaxAPILevel = oldAPI
48+
result = append(result, old)
49+
50+
nw := newParams[i]
51+
nw.MinAPILevel = newAPI
52+
result = append(result, nw)
53+
}
54+
55+
// Trailing params only in the new version.
56+
for i := minLen; i < len(newParams); i++ {
57+
p := newParams[i]
58+
p.MinAPILevel = newAPI
59+
result = append(result, p)
3160
}
61+
62+
// Trailing params only in the old version (removed).
63+
for i := minLen; i < len(oldParams); i++ {
64+
p := oldParams[i]
65+
p.MaxAPILevel = oldAPI
66+
result = append(result, p)
67+
}
68+
3269
return result
3370
}
3471

72+
// paramsChanged reports whether old and new param lists differ in length
73+
// or in the type at any shared position.
74+
func paramsChanged(
75+
old []spec.ParamSpec,
76+
new []spec.ParamSpec,
77+
) bool {
78+
if len(old) != len(new) {
79+
return true
80+
}
81+
for i := range old {
82+
if !old[i].Type.Equal(new[i].Type) {
83+
return true
84+
}
85+
}
86+
return false
87+
}
88+
3589
// diffBaselineParams parses AIDL files from the baseline 3rdparty directory,
3690
// converts them to specs, and diffs each interface method's params against
3791
// the current (new) specs. Any trailing params added in the new API get
@@ -121,7 +175,7 @@ func diffBaselineParams(
121175
// is new (handled by ResolveCode returning an error).
122176
continue
123177
}
124-
if len(m.Params) > len(baselineMethod.Params) {
178+
if paramsChanged(baselineMethod.Params, m.Params) {
125179
m.Params = diffMethodParams(baselineMethod, *m, baselineAPI, newAPI)
126180
diffCount++
127181
}

tools/cmd/aidl2spec/version_diff_test.go

Lines changed: 268 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/stretchr/testify/require"
99
)
1010

11-
func TestDiffMethodParams(t *testing.T) {
11+
func TestDiffMethodParams_TrailingAddition(t *testing.T) {
1212
oldMethod := spec.MethodSpec{
1313
Name: "registerClient",
1414
Params: []spec.ParamSpec{
@@ -31,9 +31,12 @@ func TestDiffMethodParams(t *testing.T) {
3131

3232
result := diffMethodParams(oldMethod, newMethod, 34, 36)
3333
require.Len(t, result, 5)
34-
assert.Equal(t, 0, result[0].MinAPILevel) // appId: present in both
35-
assert.Equal(t, 0, result[3].MinAPILevel) // transport: present in both
34+
assert.Equal(t, 0, result[0].MinAPILevel) // appId: present in both
35+
assert.Equal(t, 0, result[0].MaxAPILevel)
36+
assert.Equal(t, 0, result[3].MinAPILevel) // transport: present in both
37+
assert.Equal(t, 0, result[3].MaxAPILevel)
3638
assert.Equal(t, 36, result[4].MinAPILevel) // attributionSource: added in 36
39+
assert.Equal(t, 0, result[4].MaxAPILevel)
3740
}
3841

3942
func TestDiffMethodParams_NoChange(t *testing.T) {
@@ -48,7 +51,9 @@ func TestDiffMethodParams_NoChange(t *testing.T) {
4851
result := diffMethodParams(method, method, 34, 36)
4952
require.Len(t, result, 2)
5053
assert.Equal(t, 0, result[0].MinAPILevel)
54+
assert.Equal(t, 0, result[0].MaxAPILevel)
5155
assert.Equal(t, 0, result[1].MinAPILevel)
56+
assert.Equal(t, 0, result[1].MaxAPILevel)
5257
}
5358

5459
func TestDiffMethodParams_AllNew(t *testing.T) {
@@ -66,5 +71,265 @@ func TestDiffMethodParams_AllNew(t *testing.T) {
6671
result := diffMethodParams(oldMethod, newMethod, 34, 36)
6772
require.Len(t, result, 2)
6873
assert.Equal(t, 36, result[0].MinAPILevel)
74+
assert.Equal(t, 0, result[0].MaxAPILevel)
6975
assert.Equal(t, 36, result[1].MinAPILevel)
76+
assert.Equal(t, 0, result[1].MaxAPILevel)
77+
}
78+
79+
func TestDiffMethodParams_ParamRemoval(t *testing.T) {
80+
oldMethod := spec.MethodSpec{
81+
Name: "configure",
82+
Params: []spec.ParamSpec{
83+
{Name: "id", Type: spec.TypeRef{Name: "int"}},
84+
{Name: "flags", Type: spec.TypeRef{Name: "int"}},
85+
{Name: "legacy", Type: spec.TypeRef{Name: "String"}},
86+
},
87+
}
88+
newMethod := spec.MethodSpec{
89+
Name: "configure",
90+
Params: []spec.ParamSpec{
91+
{Name: "id", Type: spec.TypeRef{Name: "int"}},
92+
},
93+
}
94+
95+
result := diffMethodParams(oldMethod, newMethod, 34, 36)
96+
require.Len(t, result, 3)
97+
98+
// id: unchanged
99+
assert.Equal(t, "id", result[0].Name)
100+
assert.Equal(t, 0, result[0].MinAPILevel)
101+
assert.Equal(t, 0, result[0].MaxAPILevel)
102+
103+
// flags: removed (only in old)
104+
assert.Equal(t, "flags", result[1].Name)
105+
assert.Equal(t, 0, result[1].MinAPILevel)
106+
assert.Equal(t, 34, result[1].MaxAPILevel)
107+
108+
// legacy: removed (only in old)
109+
assert.Equal(t, "legacy", result[2].Name)
110+
assert.Equal(t, 0, result[2].MinAPILevel)
111+
assert.Equal(t, 34, result[2].MaxAPILevel)
112+
}
113+
114+
func TestDiffMethodParams_TypeChange(t *testing.T) {
115+
oldMethod := spec.MethodSpec{
116+
Name: "setConfig",
117+
Params: []spec.ParamSpec{
118+
{Name: "key", Type: spec.TypeRef{Name: "String"}},
119+
{Name: "value", Type: spec.TypeRef{Name: "int"}},
120+
},
121+
}
122+
newMethod := spec.MethodSpec{
123+
Name: "setConfig",
124+
Params: []spec.ParamSpec{
125+
{Name: "key", Type: spec.TypeRef{Name: "String"}},
126+
{Name: "value", Type: spec.TypeRef{Name: "ParcelableConfig"}},
127+
},
128+
}
129+
130+
result := diffMethodParams(oldMethod, newMethod, 34, 36)
131+
require.Len(t, result, 3)
132+
133+
// key: unchanged
134+
assert.Equal(t, "key", result[0].Name)
135+
assert.Equal(t, "String", result[0].Type.Name)
136+
assert.Equal(t, 0, result[0].MinAPILevel)
137+
assert.Equal(t, 0, result[0].MaxAPILevel)
138+
139+
// value (old variant): capped at oldAPI
140+
assert.Equal(t, "value", result[1].Name)
141+
assert.Equal(t, "int", result[1].Type.Name)
142+
assert.Equal(t, 0, result[1].MinAPILevel)
143+
assert.Equal(t, 34, result[1].MaxAPILevel)
144+
145+
// value (new variant): starts at newAPI
146+
assert.Equal(t, "value", result[2].Name)
147+
assert.Equal(t, "ParcelableConfig", result[2].Type.Name)
148+
assert.Equal(t, 36, result[2].MinAPILevel)
149+
assert.Equal(t, 0, result[2].MaxAPILevel)
150+
}
151+
152+
func TestDiffMethodParams_Mixed(t *testing.T) {
153+
// Scenario: 4 old params, 3 new params.
154+
// Position 0: same type (unchanged)
155+
// Position 1: different type (type change)
156+
// Position 2: same type (unchanged)
157+
// Position 3: only in old (removed)
158+
// No trailing additions in new.
159+
oldMethod := spec.MethodSpec{
160+
Name: "complexMethod",
161+
Params: []spec.ParamSpec{
162+
{Name: "ctx", Type: spec.TypeRef{Name: "IBinder"}},
163+
{Name: "config", Type: spec.TypeRef{Name: "int"}},
164+
{Name: "name", Type: spec.TypeRef{Name: "String"}},
165+
{Name: "debug", Type: spec.TypeRef{Name: "boolean"}},
166+
},
167+
}
168+
newMethod := spec.MethodSpec{
169+
Name: "complexMethod",
170+
Params: []spec.ParamSpec{
171+
{Name: "ctx", Type: spec.TypeRef{Name: "IBinder"}},
172+
{Name: "config", Type: spec.TypeRef{Name: "Bundle"}},
173+
{Name: "name", Type: spec.TypeRef{Name: "String"}},
174+
},
175+
}
176+
177+
result := diffMethodParams(oldMethod, newMethod, 34, 36)
178+
179+
// Expected: ctx, config(old), config(new), name, debug(removed) = 5 entries.
180+
require.Len(t, result, 5)
181+
182+
// ctx: unchanged
183+
assert.Equal(t, "ctx", result[0].Name)
184+
assert.Equal(t, "IBinder", result[0].Type.Name)
185+
assert.Equal(t, 0, result[0].MinAPILevel)
186+
assert.Equal(t, 0, result[0].MaxAPILevel)
187+
188+
// config (old variant): capped
189+
assert.Equal(t, "config", result[1].Name)
190+
assert.Equal(t, "int", result[1].Type.Name)
191+
assert.Equal(t, 0, result[1].MinAPILevel)
192+
assert.Equal(t, 34, result[1].MaxAPILevel)
193+
194+
// config (new variant): starts at newAPI
195+
assert.Equal(t, "config", result[2].Name)
196+
assert.Equal(t, "Bundle", result[2].Type.Name)
197+
assert.Equal(t, 36, result[2].MinAPILevel)
198+
assert.Equal(t, 0, result[2].MaxAPILevel)
199+
200+
// name: unchanged
201+
assert.Equal(t, "name", result[3].Name)
202+
assert.Equal(t, "String", result[3].Type.Name)
203+
assert.Equal(t, 0, result[3].MinAPILevel)
204+
assert.Equal(t, 0, result[3].MaxAPILevel)
205+
206+
// debug: removed
207+
assert.Equal(t, "debug", result[4].Name)
208+
assert.Equal(t, "boolean", result[4].Type.Name)
209+
assert.Equal(t, 0, result[4].MinAPILevel)
210+
assert.Equal(t, 34, result[4].MaxAPILevel)
211+
}
212+
213+
func TestDiffMethodParams_MixedWithTrailingAddition(t *testing.T) {
214+
// Position 0: same (unchanged)
215+
// Position 1: different type (type change)
216+
// Position 2: only in new (trailing addition)
217+
oldMethod := spec.MethodSpec{
218+
Name: "update",
219+
Params: []spec.ParamSpec{
220+
{Name: "id", Type: spec.TypeRef{Name: "int"}},
221+
{Name: "data", Type: spec.TypeRef{Name: "byte", IsArray: true}},
222+
},
223+
}
224+
newMethod := spec.MethodSpec{
225+
Name: "update",
226+
Params: []spec.ParamSpec{
227+
{Name: "id", Type: spec.TypeRef{Name: "int"}},
228+
{Name: "data", Type: spec.TypeRef{Name: "ParcelFileDescriptor"}},
229+
{Name: "flags", Type: spec.TypeRef{Name: "int"}},
230+
},
231+
}
232+
233+
result := diffMethodParams(oldMethod, newMethod, 34, 36)
234+
235+
// Expected: id, data(old), data(new), flags = 4 entries.
236+
require.Len(t, result, 4)
237+
238+
// id: unchanged
239+
assert.Equal(t, "id", result[0].Name)
240+
assert.Equal(t, 0, result[0].MinAPILevel)
241+
assert.Equal(t, 0, result[0].MaxAPILevel)
242+
243+
// data (old): capped
244+
assert.Equal(t, "data", result[1].Name)
245+
assert.Equal(t, "byte", result[1].Type.Name)
246+
assert.Equal(t, 0, result[1].MinAPILevel)
247+
assert.Equal(t, 34, result[1].MaxAPILevel)
248+
249+
// data (new): starts at newAPI
250+
assert.Equal(t, "data", result[2].Name)
251+
assert.Equal(t, "ParcelFileDescriptor", result[2].Type.Name)
252+
assert.Equal(t, 36, result[2].MinAPILevel)
253+
assert.Equal(t, 0, result[2].MaxAPILevel)
254+
255+
// flags: trailing addition
256+
assert.Equal(t, "flags", result[3].Name)
257+
assert.Equal(t, 36, result[3].MinAPILevel)
258+
assert.Equal(t, 0, result[3].MaxAPILevel)
259+
}
260+
261+
func TestDiffMethodParams_GenericTypeArgs(t *testing.T) {
262+
// Verify that TypeRef.Equal compares TypeArgs recursively.
263+
oldMethod := spec.MethodSpec{
264+
Name: "getItems",
265+
Params: []spec.ParamSpec{
266+
{
267+
Name: "items",
268+
Type: spec.TypeRef{
269+
Name: "List",
270+
TypeArgs: []spec.TypeRef{{Name: "String"}},
271+
},
272+
},
273+
},
274+
}
275+
newMethod := spec.MethodSpec{
276+
Name: "getItems",
277+
Params: []spec.ParamSpec{
278+
{
279+
Name: "items",
280+
Type: spec.TypeRef{
281+
Name: "List",
282+
TypeArgs: []spec.TypeRef{{Name: "ParcelableItem"}},
283+
},
284+
},
285+
},
286+
}
287+
288+
result := diffMethodParams(oldMethod, newMethod, 34, 36)
289+
require.Len(t, result, 2)
290+
291+
// old variant: List<String> capped
292+
assert.Equal(t, "List", result[0].Type.Name)
293+
assert.Equal(t, "String", result[0].Type.TypeArgs[0].Name)
294+
assert.Equal(t, 34, result[0].MaxAPILevel)
295+
296+
// new variant: List<ParcelableItem> starts
297+
assert.Equal(t, "List", result[1].Type.Name)
298+
assert.Equal(t, "ParcelableItem", result[1].Type.TypeArgs[0].Name)
299+
assert.Equal(t, 36, result[1].MinAPILevel)
300+
}
301+
302+
func TestParamsChanged(t *testing.T) {
303+
t.Run("equal", func(t *testing.T) {
304+
params := []spec.ParamSpec{
305+
{Name: "a", Type: spec.TypeRef{Name: "int"}},
306+
{Name: "b", Type: spec.TypeRef{Name: "String"}},
307+
}
308+
assert.False(t, paramsChanged(params, params))
309+
})
310+
311+
t.Run("different_length", func(t *testing.T) {
312+
old := []spec.ParamSpec{
313+
{Name: "a", Type: spec.TypeRef{Name: "int"}},
314+
}
315+
new := []spec.ParamSpec{
316+
{Name: "a", Type: spec.TypeRef{Name: "int"}},
317+
{Name: "b", Type: spec.TypeRef{Name: "String"}},
318+
}
319+
assert.True(t, paramsChanged(old, new))
320+
})
321+
322+
t.Run("different_type", func(t *testing.T) {
323+
old := []spec.ParamSpec{
324+
{Name: "a", Type: spec.TypeRef{Name: "int"}},
325+
}
326+
new := []spec.ParamSpec{
327+
{Name: "a", Type: spec.TypeRef{Name: "long"}},
328+
}
329+
assert.True(t, paramsChanged(old, new))
330+
})
331+
332+
t.Run("both_empty", func(t *testing.T) {
333+
assert.False(t, paramsChanged(nil, nil))
334+
})
70335
}

0 commit comments

Comments
 (0)