Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ jobs:
shell: bash
run: ./gradlew test --no-daemon --stacktrace

- name: Report test results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: de.peeeq.wurstscript/build/test-results/**/*.xml
check_name: Test Results (${{ matrix.os }})
fail_on_failure: false

- name: Upload packaged artifact (per-OS)
uses: actions/upload-artifact@v4
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ public static FuncLink create(FunctionDefinition func, WScope definedIn) {
WurstType returnType = func.attrReturnTyp();
WurstType receiverType = calcReceiverType(definedIn, func);

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

return new FuncLink(visibility, definedIn, typeParams, receiverType, func, paramNames, paramTypes, returnType, mapping);
}
Expand Down Expand Up @@ -182,6 +185,16 @@ public FuncLink withTypeArgBinding(Element context, VariableBinding binding) {
}
WurstType newReceiverType = adjustType(context, getReceiverType(), binding);
changed = changed || newReceiverType != getReceiverType();
// Also check if any type params are being bound (even if types don't
// change structurally — e.g., T bound to itself within a generic module)
if (!changed) {
for (TypeParamDef tp : getTypeParams()) {
if (binding.contains(tp)) {
changed = true;
break;
}
}
}
if (changed) {
// remove type parameters that are now bound:
List<TypeParamDef> newTypeParams = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,20 @@ public static List<String> getParamNames(WParameters parameters) {

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


Expand Down Expand Up @@ -234,7 +246,6 @@ public WurstType getVarargType() {
for (int i = 0; i < argTypes.size(); i++) {
WurstType pt = getParamType(i);
WurstType at = argTypes.get(i);
mapping = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT);
VariableBinding before = mapping;
VariableBinding after = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT);
WLogger.trace(() -> "[IMPLCONV] vb " + System.identityHashCode(before)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,76 @@ public void biggerModule() {
);
}

// Regression: FuncLink.create() was seeding the mapping with ALL visible type params
// (including enclosing module/class type params), causing them to appear as unbound
// inference variables on generic function calls. Only the function's own type params
// should be seeded; module type params are resolved via receiver type matching.
@Test
public void genericModuleFunctionWithOwnTypeParam() {
testAssertOkLines(false,
"package test",
" native testSuccess()",
" module M<T>",
" T value",
" function map<S>(S s) returns S",
" return s",
" class C",
" use M<int>",
" init",
" C c = new C",
" int r = c.map(42)",
" if r == 42",
" testSuccess()",
"endpackage"
);
}

// Regression: FuncLink.withTypeArgBinding() only detected structural type changes,
// missing the case where a type param is bound to itself (e.g. T→T in a generic class
// using a generic module). The FuncLink was not updated, leaving stale type params.
@Test
public void genericModuleInGenericClassGet() {
testAssertOkLines(false,
"package test",
" native testSuccess()",
" module M<T>",
" T value",
" function get() returns T",
" return value",
" class C<S>",
" use M<S>",
" init",
" C<int> c = new C<int>",
" c.value = 42",
" int v = c.get()",
" if v == 42",
" testSuccess()",
"endpackage"
);
}

// Regression: static generic function in a generic module caused NPE due to
// duplicate matchAgainstSupertype call passing potentially-null mapping to the
// second call. FunctionSignature.fromNameLink also incorrectly added enclosing
// module type params as inference variables.
@Test
public void staticGenericFunctionInGenericModule() {
testAssertOkLines(false,
"package test",
" native testSuccess()",
" module M<T>",
" static function wrap<S>(S s) returns S",
" return s",
" class C",
" use M<int>",
" init",
" int x = C.wrap(99)",
" if x == 99",
" testSuccess()",
"endpackage"
);
}

@Test
public void genericInception() {
testAssertOkLines(false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1212,4 +1212,21 @@ public void nestedList2() {
);
}

@Test
public void conflictingTypeArgsMemberMethod() {
// Regression test: calling a generic method via dot notation with conflicting type arguments
// used to cause a NullPointerException instead of a proper type error.
// The two arguments infer conflicting types for T (int vs string), which should produce
// a "Wrong parameter type" compile error, not a compiler crash.
testAssertErrorsLines(false, "Wrong parameter type",
"package test",
"class C",
" function combine<T>(T x, T y) returns T",
" return x",
"init",
" let c = new C()",
" c.combine(1, \"hello\")"
);
}

}
Loading