Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit 8b1b050

Browse files
authored
[generator] Fix parsing of complex generic types (#729)
Fixes: #728 Context: https://xamarin.github.io/bugzilla-archives/12/12468/bug.html#c1 In order to avoid CS0234 errors, we always write non-intrinsic types prefixed with `global::`. For example, instead of System.Collections.Generic.List<Java.Lang.Object> `generator` emits global::System.Collections.Generic.List<global::Java.Lang.Object> To do this, we have to parse a type into its component types so we can add `global::` to each part. However, our parsing algorithm struggles with generic types with multiple type arguments. Thus, this: System.Collections.Generic.IDictionary<System.Collections.Generic.IList<string>, Kotlin.Pair> loses the second template parameter, becoming: global::System.Collections.Generic.IDictionary<global::System.Collections.Generic.IList<string>> while this: System.Collections.Generic.IDictionary<System.Collections.Generic.IList<string>, System.Collections.Generic.IList<Kotlin.Pair>> throws an `ArgumentOutOfRangeException`, which is the cause of #728. Fix the `ArgumentOutOfRangeException` by introducing a new recursive parser that better understands generic types.
1 parent ee7afee commit 8b1b050

3 files changed

Lines changed: 168 additions & 32 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
usingSystem;
2+
usingMonoDroid.Generation;
3+
usingNUnit.Framework;
4+
5+
namespacegeneratortests
6+
{
7+
[TestFixture]
8+
publicclassCodeGenerationOptionsTests
9+
{
10+
[Test]
11+
publicvoidGetOutputNameUseGlobal()
12+
{
13+
varopt=newCodeGenerationOptions{UseGlobal=true};
14+
15+
Assert.AreEqual(string.Empty,opt.GetOutputName(string.Empty));
16+
Assert.AreEqual("int",opt.GetOutputName("int"));
17+
Assert.AreEqual("void",opt.GetOutputName("void"));
18+
Assert.AreEqual("void",opt.GetOutputName("System.Void"));
19+
Assert.AreEqual("params int[]",opt.GetOutputName("params int[]"));
20+
Assert.AreEqual("params global::System.Object[]",opt.GetOutputName("params System.Object[]"));
21+
Assert.AreEqual("int[][][]",opt.GetOutputName("int[][][]"));
22+
Assert.AreEqual("global::System.Object[][][]",opt.GetOutputName("System.Object[][][]"));
23+
24+
Assert.AreEqual("global::System.Collections.Generic.List<string[]>",
25+
opt.GetOutputName("System.Collections.Generic.List<string[]>"));
26+
27+
Assert.AreEqual("global::System.Collections.Generic.Dictionary<string, string>",
28+
opt.GetOutputName("System.Collections.Generic.Dictionary<string, string>"));
29+
30+
Assert.AreEqual("global::System.Collections.Generic.List<global::System.Collections.Generic.List<string>>",
31+
opt.GetOutputName("System.Collections.Generic.List<System.Collections.Generic.List<string>>"));
32+
33+
Assert.AreEqual("global::System.Collections.Generic.List<global::System.Collections.Generic.Dictionary<string, global::System.Collections.Generic.Dictionary<global::System.Object, global::System.Object>>>",
34+
opt.GetOutputName("System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.Object, System.Object>>>"));
35+
36+
Assert.AreEqual("global::System.Collections.Generic.IList<global::Kotlin.Pair>",
37+
opt.GetOutputName("System.Collections.Generic.IList<Kotlin.Pair>"));
38+
39+
Assert.AreEqual("global::System.Collections.Generic.IDictionary<string, global::System.Collections.Generic.IList<global::Kotlin.Pair>>",
40+
opt.GetOutputName("System.Collections.Generic.IDictionary<string, System.Collections.Generic.IList<Kotlin.Pair>>"));
41+
42+
Assert.AreEqual("global::System.Collections.Generic.IDictionary<global::System.Collections.Generic.IList<string>, global::Kotlin.Pair>",
43+
opt.GetOutputName("System.Collections.Generic.IDictionary<System.Collections.Generic.IList<string>, Kotlin.Pair>"));
44+
45+
Assert.AreEqual("global::System.Collections.Generic.IDictionary<global::System.Collections.Generic.IList<string>, global::System.Collections.Generic.IList<global::Kotlin.Pair>>",
46+
opt.GetOutputName("System.Collections.Generic.IDictionary<System.Collections.Generic.IList<string>, System.Collections.Generic.IList<Kotlin.Pair>>"));
47+
48+
Assert.AreEqual("global::System.Collections.Generic.List<global::System.Collections.Generic.List<string>[]>[]",
49+
opt.GetOutputName("System.Collections.Generic.List<System.Collections.Generic.List<string>[]>[]"));
50+
51+
Assert.AreEqual("global::System.Collections.Generic.List<global::System.Collections.Generic.List<string>.Enumerator[]>",
52+
opt.GetOutputName("System.Collections.Generic.List<System.Collections.Generic.List<string>.Enumerator[]>"));
53+
}
54+
}
55+
}

‎tools/generator/CodeGenerationOptions.cs‎

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -212,42 +212,20 @@ internal IEnumerable<string> GetJniMarshalDelegates ()
212212
returnjni_marshal_delegates;
213213
}
214214

215-
publicstringGetOutputName(strings)
215+
publicstringGetOutputName(stringtype)
216216
{
217-
if(s=="System.Void")
217+
// Handle a few special cases
218+
if(type=="System.Void")
218219
return"void";
219-
if(s.StartsWith("params "))
220-
return"params "+GetOutputName(s.Substring("params ".Length));
221-
if(s.StartsWith("global::"))
220+
if(type.StartsWith("params "))
221+
return"params "+GetOutputName(type.Substring("params ".Length));
222+
if(type.StartsWith("global::"))
222223
Report.LogCodedError(Report.ErrorUnexpectedGlobal);
223224
if(!UseGlobal)
224-
returns;
225-
intidx=s.IndexOf('<');
226-
if(idx<0){
227-
if(s.IndexOf('.')<0)
228-
returns;// hack, to prevent things like global::int
229-
return"global::"+s;
230-
}
231-
intidx2=s.LastIndexOf('>');
232-
stringsub=s.Substring(idx+1,idx2-idx-1);
233-
vartypeParams=newList<string>();
234-
while(true){
235-
intidx3=sub.IndexOf('<');
236-
intidx4=sub.IndexOf(',');
237-
if(idx4<0){
238-
typeParams.Add(GetOutputName(sub));
239-
break;
240-
}elseif(idx3<0||idx4<idx3){// more than one type params.
241-
typeParams.Add(GetOutputName(sub.Substring(0,idx4)));
242-
if(idx4+1==sub.Length)
243-
break;
244-
sub=sub.Substring(idx4+1).Trim();
245-
}else{
246-
typeParams.Add(GetOutputName(sub));
247-
break;
248-
}
249-
}
250-
returnGetOutputName(s.Substring(0,idx))+'<'+String.Join(", ",typeParams.ToArray())+'>';
225+
returntype;
226+
227+
// Add "global::" in front of types
228+
returnParsedType.Parse(type).ToString(UseGlobal);
251229
}
252230

253231
publicstringGetSafeIdentifier(stringname)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
usingSystem.Collections.Generic;
2+
usingSystem.Linq;
3+
4+
namespaceMonoDroid.Generation
5+
{
6+
// Parses a type string into its type and optionally its generic type arguments
7+
// ex: Dictionary<string, List<string>>
8+
// -> Type: Dictionary<{0}>
9+
// - GenericArguments:
10+
// - Type: string
11+
// - Type: List<{0}>
12+
// - GenericArguments:
13+
// - Type: string
14+
// A placeholder "{0}" is added because the type may extend past the generics:
15+
// ex: "List<string>.Enumerator[]" becomes "List<{0}>.Enumerator[]"
16+
publicclassParsedType
17+
{
18+
publicstringType{get;set;}
19+
publicList<ParsedType>GenericArguments{get;}=newList<ParsedType>();
20+
publicboolHasGenerics=>GenericArguments.Count>0;
21+
22+
ParsedType(){}
23+
24+
publicstaticParsedTypeParse(stringtype)
25+
{
26+
varless_than=type.IndexOf('<');
27+
28+
// No generics
29+
if(less_than<0)
30+
returnnewParsedType{Type=type};
31+
32+
vargreater_than=type.LastIndexOf('>');
33+
vartype_args=type.Substring(less_than+1,greater_than-less_than-1);
34+
vartype_string=type.Substring(0,less_than)+"<{0}>"+(greater_than+1<type.Length?type.Substring(greater_than+1):string.Empty);
35+
36+
varparsed_args=ParseTypeList(type_args);
37+
38+
vart=newParsedType{Type=type_string};
39+
40+
foreach(varpinparsed_args)
41+
t.GenericArguments.Add(Parse(p));
42+
43+
returnt;
44+
}
45+
46+
publicoverridestringToString()
47+
{
48+
returnToString(false);
49+
}
50+
51+
publicstringToString(booluseGlobal=false)
52+
{
53+
vartype=(useGlobal&&Type.IndexOf('.')>=0?"global::":string.Empty)+Type;
54+
55+
if(!HasGenerics)
56+
returntype;
57+
58+
returntype.Replace("{0}",string.Join(", ",GenericArguments.Select(p =>p.ToString(useGlobal))));
59+
}
60+
61+
staticList<string>ParseTypeList(stringtype)
62+
{
63+
varlist=newList<string>();
64+
65+
// Only one type
66+
if(type.IndexOf(',')<0){
67+
list.Add(type);
68+
returnlist;
69+
}
70+
71+
// Remove any whitespace
72+
type=type.Replace(" ","");
73+
74+
varstart=0;
75+
varcounter=-1;
76+
vardepth=0;
77+
78+
while(++counter<type.Length){
79+
if(type[counter]=='<'){
80+
depth++;
81+
continue;
82+
}
83+
84+
if(type[counter]=='>'){
85+
depth--;
86+
continue;
87+
}
88+
89+
// This is a list separator, add the previous type
90+
if(depth==0&&type[counter]==','){
91+
list.Add(type.Substring(start,counter-start));
92+
start=counter+1;
93+
continue;
94+
}
95+
}
96+
97+
// Add the final type
98+
list.Add(type.Substring(start,counter-start));
99+
100+
returnlist;
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)