Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler): enforce super constructor call in derived class constructors #5354

8 changes: 8 additions & 0 deletions examples/tests/invalid/class.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ class Four extends One {
}
}

class Five extends One {
someStr: str;
new(someNum: num, someStr: str) {
this.someStr = someStr;
}
//^ Super call missing in initializer of derived class
}

// Super inflight
inflight class Plane {
year: num;
Expand Down
1 change: 1 addition & 0 deletions examples/tests/valid/function_variadic_arguments.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class A {

class B extends A {
new(msg: str) {
super(msg);
this.message = msg;
}
}
Expand Down
11 changes: 9 additions & 2 deletions examples/tests/valid/optionals.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ class Super {
new() { this.name = "Super"; }
}
class Sub extends Super {
new() { this.name = "Sub"; }
new() {
super();
this.name = "Sub";
}
}
class Sub1 extends Super {
new() { this.name = "Sub"; }
new() {
super();
this.name = "Sub";
}

}

let optionalSup: Super? = new Super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ source: libs/wingc/src/jsify/tests.rs
class Derived extends Base {
inflight g: str;
inflight new() {
super();
this.g = "world";
}

Expand Down Expand Up @@ -59,6 +60,7 @@ module.exports = function({ $Base }) {
this.g;
}
async $inflight_init() {
super($scope, $id, );
this.g = "world";
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ source: libs/wingc/src/jsify/tests.rs
class Derived extends Base {
g: str;
new() {
super();
this.g = "world";
}

Expand Down Expand Up @@ -98,7 +99,7 @@ class $Root extends $stdlib.std.Resource {
}
class Derived extends Base {
constructor($scope, $id, ) {
super($scope, $id);
super($scope, $id, );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be avoided? Not critical if it's a hassle

Copy link
Collaborator Author

@fynnfluegge fynnfluegge Dec 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering about his change, it was generated after I added super() to the inflight constructor and I ran e2e tests. I don't know if this is critical, but seems suspicious to me, so I pushed it to see if somebody knows whats going on 🙂
Screenshot 2023-12-28 at 11 16 45

this.g = "world";
}
foo() {
Expand Down
2 changes: 2 additions & 0 deletions libs/wingc/src/jsify/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,7 @@ fn base_class_with_fields_preflight() {
class Derived extends Base {
g: str;
new() {
super();
this.g = "world";
}

Expand All @@ -1552,6 +1553,7 @@ fn base_class_with_fields_inflight() {
class Derived extends Base {
inflight g: str;
inflight new() {
super();
this.g = "world";
}

Expand Down
80 changes: 80 additions & 0 deletions libs/wingc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ impl<'s> Parser<'s> {
_ => Scope::empty(),
};

self.report_missing_super_call(&root, &scope);

// Module files can only have certain kinds of statements
if !is_entrypoint_file(&self.source_name) {
for stmt in &scope.statements {
Expand Down Expand Up @@ -480,6 +482,84 @@ impl<'s> Parser<'s> {
}
}

fn report_missing_super_call(&self, node: &Node, scope: &Scope) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change this logic. Instead of re-scanning the entire tree I'd just modify build_class_statement. There you can look at the returned value from build_scope when creating the scope for the initializer. If the first statement (or actually any statement) is a StmtKind::SuperConstructor and we have a defined parent for the class then you're good to go.

let mut cursor = node.walk();
let mut nodes = Vec::new();
for child in node.named_children(&mut cursor) {
if child.is_extra() {
continue;
}
match child.kind() {
"class_definition" => {
let mut cursor = child.walk();
let mut has_extends = false;
child.children(&mut cursor).for_each(|child| {
if child.kind() == "extends" {
has_extends = true;
}
if child.kind() == "class_implementation" && has_extends {
let mut cursor = child.walk();
child.children(&mut cursor).for_each(|child| {
if child.kind() == "initializer" || child.kind() == "inflight_initializer" {
nodes.push(child);
}
});
}
});
}
_ => {}
}
}

if nodes.len() == 0 {
return;
}
let mut counter = 0;
for stmt in &scope.statements {
let stmt_kind = &stmt.kind;
match stmt_kind {
StmtKind::Class(class) => {
if class.parent.is_some() {
let mut super_call_present = false;
let mut initializer_present = false;
match &class.initializer.body {
FunctionBody::Statements(statements) => {
self.has_super_constructor(&statements, &mut initializer_present, &mut super_call_present);
}
_ => {}
}
match &class.inflight_initializer.body {
FunctionBody::Statements(statements) => {
self.has_super_constructor(&statements, &mut initializer_present, &mut super_call_present);
}
_ => {}
}
if !super_call_present && initializer_present {
self.add_error("Super call missing in initializer of derived class", &nodes[counter]);
}
if initializer_present {
counter += 1;
}
}
}
_ => {}
}
}
}

fn has_super_constructor(&self, scope: &Scope, initializer_present: &mut bool, super_call_present: &mut bool) {
if &scope.statements.len() > &0 {
*initializer_present = true;
let first_statement = &scope.statements.first().unwrap().kind;
match first_statement {
StmtKind::SuperConstructor { arg_list: _ } => {
*super_call_present = true;
}
_ => {}
}
}
}

fn node_text<'a>(&'a self, node: &Node) -> &'a str {
return str::from_utf8(&self.source[node.byte_range()]).unwrap();
}
Expand Down
32 changes: 26 additions & 6 deletions tools/hangar/__snapshots__/invalid.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -712,12 +712,32 @@ error: Call to super constructor can only be done from within a class constructo


error: Reserved method name. Initializers are declared with \\"init\\"
--> ../../../examples/tests/invalid/class.test.w:160:3
--> ../../../examples/tests/invalid/class.test.w:168:3
|
160 | constructor() {
168 | constructor() {
| ^^^^^^^^^^^ Reserved method name. Initializers are declared with \\"init\\"


error: Super call missing in initializer of derived class
--> ../../../examples/tests/invalid/class.test.w:96:3
|
96 | / new(name: str, major: str, hrlyWage: num) {
97 | | this.hrlyWage = hrlyWage;
98 | | super(name, major);
99 | | // ^^^^^^^^^^^^^^^^^^^ Expected call to super to be first statement in constructor
100 | | }
| \\\\---^ Super call missing in initializer of derived class


error: Super call missing in initializer of derived class
--> ../../../examples/tests/invalid/class.test.w:149:3
|
149 | / new(someNum: num, someStr: str) {
150 | | this.someStr = someStr;
151 | | }
| \\\\---^ Super call missing in initializer of derived class


error: Expected block
--> ../../../examples/tests/invalid/class.test.w:17:16
|
Expand Down Expand Up @@ -845,9 +865,9 @@ error: Unknown symbol \\"C11\\"


error: Cannot instantiate abstract class \\"Resource\\"
--> ../../../examples/tests/invalid/class.test.w:165:1
--> ../../../examples/tests/invalid/class.test.w:173:1
|
165 | new std.Resource();
173 | new std.Resource();
| ^^^^^^^^^^^^^^^^^^ Cannot instantiate abstract class \\"Resource\\"


Expand Down Expand Up @@ -887,9 +907,9 @@ error: Expected 1 positional argument(s) but got 2


error: Expected 1 positional argument(s) but got 0
--> ../../../examples/tests/invalid/class.test.w:157:5
--> ../../../examples/tests/invalid/class.test.w:165:5
|
157 | super();
165 | super();
| ^^^^^^^^ Expected 1 positional argument(s) but got 0


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class $Root extends $stdlib.std.Resource {
}
class B extends A {
constructor($scope, $id, msg) {
super($scope, $id);
super($scope, $id, msg);
this.message = msg;
}
static _toInflightType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class $Root extends $stdlib.std.Resource {
}
class Sub extends Super {
constructor($scope, $id, ) {
super($scope, $id);
super($scope, $id, );
this.name = "Sub";
}
static _toInflightType() {
Expand All @@ -182,7 +182,7 @@ class $Root extends $stdlib.std.Resource {
}
class Sub1 extends Super {
constructor($scope, $id, ) {
super($scope, $id);
super($scope, $id, );
this.name = "Sub";
}
static _toInflightType() {
Expand Down