Skip to content

Commit 6b6494f

Browse files
authored
Fix generic npe & other module type issues (#1175)
fixes #1174
1 parent 5cb29f2 commit 6b6494f

5 files changed

Lines changed: 124 additions & 5 deletions

File tree

.github/workflows/build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ jobs:
9696
shell: bash
9797
run: ./gradlew test --no-daemon --stacktrace
9898

99+
- name: Report test results
100+
if: always()
101+
uses: mikepenz/action-junit-report@v4
102+
with:
103+
report_paths: de.peeeq.wurstscript/build/test-results/**/*.xml
104+
check_name: Test Results (${{ matrix.os }})
105+
fail_on_failure: false
106+
99107
- name: Upload packaged artifact (per-OS)
100108
uses: actions/upload-artifact@v4
101109
with:

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@ public static FuncLink create(FunctionDefinition func, WScope definedIn) {
6969
WurstType returnType = func.attrReturnTyp();
7070
WurstType receiverType = calcReceiverType(definedIn, func);
7171

72-
// Seed mapping with ALL visible type vars (not just the function's)
73-
VariableBinding mapping = VariableBinding.emptyMapping().withTypeVariables(typeParams);
72+
// Only the function's OWN type params need inference via argument matching.
73+
// Enclosing structure type params (class/module) are resolved through
74+
// receiver type matching in matchDefLinkReceiver, not through inference.
75+
List<TypeParamDef> ownTypeParams = typeParams(func).collect(Collectors.toList());
76+
VariableBinding mapping = VariableBinding.emptyMapping().withTypeVariables(ownTypeParams);
7477

7578
return new FuncLink(visibility, definedIn, typeParams, receiverType, func, paramNames, paramTypes, returnType, mapping);
7679
}
@@ -182,6 +185,16 @@ public FuncLink withTypeArgBinding(Element context, VariableBinding binding) {
182185
}
183186
WurstType newReceiverType = adjustType(context, getReceiverType(), binding);
184187
changed = changed || newReceiverType != getReceiverType();
188+
// Also check if any type params are being bound (even if types don't
189+
// change structurally — e.g., T bound to itself within a generic module)
190+
if (!changed) {
191+
for (TypeParamDef tp : getTypeParams()) {
192+
if (binding.contains(tp)) {
193+
changed = true;
194+
break;
195+
}
196+
}
197+
}
185198
if (changed) {
186199
// remove type parameters that are now bound:
187200
List<TypeParamDef> newTypeParams = new ArrayList<>();

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,20 @@ public static List<String> getParamNames(WParameters parameters) {
118118

119119
public static FunctionSignature fromNameLink(FuncLink f) {
120120
VariableBinding mapping = f.getVariableBinding();
121-
mapping = mapping.withTypeVariables(f.getTypeParams());
122-
return new FunctionSignature(f.getDef(), mapping, f.getReceiverType(), f.getName(), f.getParameterTypes(), getParamNames(f.getDef().getParameters()), f.getReturnType());
121+
FunctionDefinition def = f.getDef();
122+
// Only add the function's own type params that are still unbound (i.e., still in
123+
// f.getTypeParams() — withTypeArgBinding removes them as they get resolved).
124+
// We must NOT add enclosing structure (module/class) type params, which would
125+
// appear as spurious unbound inference variables.
126+
if (def instanceof AstElementWithTypeParameters) {
127+
java.util.Set<TypeParamDef> ownParams = new java.util.HashSet<>(
128+
((AstElementWithTypeParameters) def).getTypeParameters());
129+
List<TypeParamDef> unboundOwn = f.getTypeParams().stream()
130+
.filter(ownParams::contains)
131+
.collect(Collectors.toList());
132+
mapping = mapping.withTypeVariables(unboundOwn);
133+
}
134+
return new FunctionSignature(def, mapping, f.getReceiverType(), f.getName(), f.getParameterTypes(), getParamNames(def.getParameters()), f.getReturnType());
123135
}
124136

125137

@@ -234,7 +246,6 @@ public WurstType getVarargType() {
234246
for (int i = 0; i < argTypes.size(); i++) {
235247
WurstType pt = getParamType(i);
236248
WurstType at = argTypes.get(i);
237-
mapping = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT);
238249
VariableBinding before = mapping;
239250
VariableBinding after = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT);
240251
WLogger.trace(() -> "[IMPLCONV] vb " + System.identityHashCode(before)

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsModuleTests.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,76 @@ public void biggerModule() {
4747
);
4848
}
4949

50+
// Regression: FuncLink.create() was seeding the mapping with ALL visible type params
51+
// (including enclosing module/class type params), causing them to appear as unbound
52+
// inference variables on generic function calls. Only the function's own type params
53+
// should be seeded; module type params are resolved via receiver type matching.
54+
@Test
55+
public void genericModuleFunctionWithOwnTypeParam() {
56+
testAssertOkLines(false,
57+
"package test",
58+
" native testSuccess()",
59+
" module M<T>",
60+
" T value",
61+
" function map<S>(S s) returns S",
62+
" return s",
63+
" class C",
64+
" use M<int>",
65+
" init",
66+
" C c = new C",
67+
" int r = c.map(42)",
68+
" if r == 42",
69+
" testSuccess()",
70+
"endpackage"
71+
);
72+
}
73+
74+
// Regression: FuncLink.withTypeArgBinding() only detected structural type changes,
75+
// missing the case where a type param is bound to itself (e.g. T→T in a generic class
76+
// using a generic module). The FuncLink was not updated, leaving stale type params.
77+
@Test
78+
public void genericModuleInGenericClassGet() {
79+
testAssertOkLines(false,
80+
"package test",
81+
" native testSuccess()",
82+
" module M<T>",
83+
" T value",
84+
" function get() returns T",
85+
" return value",
86+
" class C<S>",
87+
" use M<S>",
88+
" init",
89+
" C<int> c = new C<int>",
90+
" c.value = 42",
91+
" int v = c.get()",
92+
" if v == 42",
93+
" testSuccess()",
94+
"endpackage"
95+
);
96+
}
97+
98+
// Regression: static generic function in a generic module caused NPE due to
99+
// duplicate matchAgainstSupertype call passing potentially-null mapping to the
100+
// second call. FunctionSignature.fromNameLink also incorrectly added enclosing
101+
// module type params as inference variables.
102+
@Test
103+
public void staticGenericFunctionInGenericModule() {
104+
testAssertOkLines(false,
105+
"package test",
106+
" native testSuccess()",
107+
" module M<T>",
108+
" static function wrap<S>(S s) returns S",
109+
" return s",
110+
" class C",
111+
" use M<int>",
112+
" init",
113+
" int x = C.wrap(99)",
114+
" if x == 99",
115+
" testSuccess()",
116+
"endpackage"
117+
);
118+
}
119+
50120
@Test
51121
public void genericInception() {
52122
testAssertOkLines(false,

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,4 +1212,21 @@ public void nestedList2() {
12121212
);
12131213
}
12141214

1215+
@Test
1216+
public void conflictingTypeArgsMemberMethod() {
1217+
// Regression test: calling a generic method via dot notation with conflicting type arguments
1218+
// used to cause a NullPointerException instead of a proper type error.
1219+
// The two arguments infer conflicting types for T (int vs string), which should produce
1220+
// a "Wrong parameter type" compile error, not a compiler crash.
1221+
testAssertErrorsLines(false, "Wrong parameter type",
1222+
"package test",
1223+
"class C",
1224+
" function combine<T>(T x, T y) returns T",
1225+
" return x",
1226+
"init",
1227+
" let c = new C()",
1228+
" c.combine(1, \"hello\")"
1229+
);
1230+
}
1231+
12151232
}

0 commit comments

Comments
 (0)