diff --git a/spec/declaration/local_spec.lua b/spec/declaration/local_spec.lua index bcdc9850b..0fc8faff2 100644 --- a/spec/declaration/local_spec.lua +++ b/spec/declaration/local_spec.lua @@ -18,6 +18,48 @@ describe("local", function() local z: number z = x + y ]])) + + it("'type', 'record' and 'enum' are not reserved keywords", util.check([[ + local type = type + local record: string = "hello" + local enum: number = 123 + print(record) + print(enum + 123) + ]])) + + it("reports unset and untyped values as errors in tl mode", util.check_type_error([[ + local record T + x: number + y: number + end + + function T:returnsTwo(): number, number + return self.x, self.y + end + + function T:method() + local a, b = self.returnsTwo and self:returnsTwo() + end + ]], { + { msg = "assignment in declaration did not produce an initial value for variable 'b'" }, + })) + + it("reports unset values as unknown in Lua mode", util.lax_check([[ + local record T + x: number + y: number + end + + function T:returnsTwo(): number, number + return self.x, self.y + end + + function T:method() + local a, b = self.returnsTwo and self:returnsTwo() + end + ]], { + { msg = "b" }, + })) end) describe("multiple declaration", function() @@ -63,230 +105,6 @@ describe("local", function() z = x + string.byte(y) ]])) end) - - it("reports unset and untyped values as errors in tl mode", util.check_type_error([[ - local type T = record - x: number - y: number - end - - function T:returnsTwo(): number, number - return self.x, self.y - end - - function T:method() - local a, b = self.returnsTwo and self:returnsTwo() - end - ]], { - { msg = "assignment in declaration did not produce an initial value for variable 'b'" }, - })) - - it("reports unset values as unknown in Lua mode", util.lax_check([[ - local type T = record - x: number - y: number - end - - function T:returnsTwo(): number, number - return self.x, self.y - end - - function T:method() - local a, b = self.returnsTwo and self:returnsTwo() - end - ]], { - { msg = "b" }, - })) - - it("local type can declare a type alias for table", util.check([[ - local type PackTable = table.PackTable - local args: table.PackTable = table.pack(1, 2, 3) - ]])) - - it("local type can declare a nominal type alias (regression test for #238)", function () - util.mock_io(finally, { - ["module.tl"] = [[ - local record module - record Type - data: number - end - end - return module - ]], - ["main.tl"] = [[ - local module = require "module" - local type Boo = module.Type - local var: Boo = { dato = 0 } - print(var.dato) - ]], - }) - local result, err = tl.process("main.tl") - - assert.same({}, result.syntax_errors) - assert.same({ - { y = 3, x = 35, filename = "main.tl", msg = "in local declaration: var: unknown field dato" }, - { y = 4, x = 26, filename = "main.tl", msg = "invalid key 'dato' in record 'var' of type Boo" }, - }, result.type_errors) - end) - - it("local type can resolve a nominal with generics (regression test for #777)", function () - util.mock_io(finally, { - ["module.tl"] = [[ - local record module - record Foo - something: K - end - end - return module - ]], - ["main.tl"] = [[ - local module = require "module" - - local record Boo - field: MyFoo - end - - local type MyFoo = module.Foo - - local b: Boo = { field = { something = "hi" } } - local c: Boo = { field = { something = 123 } } - ]], - }) - local result, err = tl.process("main.tl") - - assert.same({}, result.syntax_errors) - assert.same({ - { y = 10, x = 55, filename = "main.tl", msg = "in record field: something: got integer, expected string" }, - }, result.type_errors) - end) - - it("catches unknown types", util.check_type_error([[ - local type MyType = UnknownType - ]], { - { msg = "unknown type UnknownType" } - })) - - it("nominal types can take type arguments", util.check([[ - local record Foo - item: R - end - - local type Foo2 = Foo - local type Bla = Foo - - local x: Bla = { item = 123 } - local y: Foo2 = { item = 123 } - ]])) - - it("types declared as nominal types are aliases", util.check([[ - local record Foo - item: R - end - - local type Foo2 = Foo - local type FooNumber = Foo - - local x: FooNumber = { item = 123 } - local y: Foo2 = { item = 123 } - - local type Foo3 = Foo - local type Foo4 = Foo2 - - local zep: Foo2 = { item = "hello" } - local zip: Foo3 = zep - local zup: Foo4 = zip - ]])) - - it("nested types can be resolved as aliases", util.check([[ - local record Foo - enum LocalEnum - "loc" - end - - record Nested - x: {LocalEnum} - y: R - end - - item: R - end - - local type Nested = Foo.Nested - ]])) - - it("'type', 'record' and 'enum' are not reserved keywords", util.check([[ - local type = type - local record: string = "hello" - local enum: number = 123 - print(record) - print(enum + 123) - ]])) - - it("local type can require a module", function () - util.mock_io(finally, { - ["class.tl"] = [[ - local record Class - data: number - end - return Class - ]], - ["main.tl"] = [[ - local type Class = require("class") - local obj: Class = { data = 2 } - ]], - }) - local result, err = tl.process("main.tl") - - assert.same({}, result.syntax_errors) - assert.same({}, result.type_errors) - end) - - it("local type can require a module and type is usable", function () - util.mock_io(finally, { - ["class.tl"] = [[ - local record Class - data: number - end - return Class - ]], - ["main.tl"] = [[ - local type Class = require("class") - local obj: Class = { invalid = 2 } - ]], - }) - local result, err = tl.process("main.tl") - - assert.same({}, result.syntax_errors) - assert.same({ - { y = 2, x = 37, filename = "main.tl", msg = "in local declaration: obj: unknown field invalid" }, - }, result.type_errors) - end) - - it("local type can require a module and its globals are visible", function () - util.mock_io(finally, { - ["class.tl"] = [[ - global record Glob - hello: number - end - - local record Class - data: number - end - return Class - ]], - ["main.tl"] = [[ - local type Class = require("class") - local obj: Glob = { hello = 2 } - local obj2: Glob = { invalid = 2 } - ]], - }) - local result, err = tl.process("main.tl") - - assert.same({}, result.syntax_errors) - assert.same({ - { y = 3, x = 37, filename = "main.tl", msg = "in local declaration: obj2: unknown field invalid" }, - }, result.type_errors) - end) end) describe("annotation", function() @@ -567,33 +385,4 @@ describe("local", function() { y = 9, msg = "_bar1: got number, expected Bar" }, { y = 10, msg = "_bar2: Foo is not a Bar" }, })) - - it("does not accept type arguments declared twice", util.check_syntax_error([[ - local type Foo = record - end - ]], { - { y = 1, msg = "cannot declare type arguments twice in type declaration" }, - })) - - it("propagates type arguments correctly", util.check_type_error([[ - local record module - record Foo - first: A - second: B - end - end - - -- note inverted arguments - local type MyFoo = module.Foo - - local record Boo - field: MyFoo - end - - local b: Boo = { field = { first = "first", second = 2 } } -- bad, not inverted! - local c: Boo = { field = { first = 1, second = "second" } } -- good, inverted! - ]], { - { y = 15, x = 42, msg = 'in record field: first: got string "first", expected integer' }, - { y = 15, x = 60, msg = 'in record field: second: got integer, expected string' }, - })) end) diff --git a/spec/declaration/local_type_spec.lua b/spec/declaration/local_type_spec.lua new file mode 100644 index 000000000..e9c0aa506 --- /dev/null +++ b/spec/declaration/local_type_spec.lua @@ -0,0 +1,215 @@ +local util = require("spec.util") +local tl = require("tl") + +describe("local type", function() + it("can declare a type alias for table", util.check([[ + local type PackTable = table.PackTable + local args: table.PackTable = table.pack(1, 2, 3) + ]])) + + it("can declare a nominal type alias (regression test for #238)", function () + util.mock_io(finally, { + ["module.tl"] = [[ + local record module + record Type + data: number + end + end + return module + ]], + ["main.tl"] = [[ + local module = require "module" + local type Boo = module.Type + local var: Boo = { dato = 0 } + print(var.dato) + ]], + }) + local result, err = tl.process("main.tl") + + assert.same({}, result.syntax_errors) + assert.same({ + { y = 3, x = 32, filename = "main.tl", msg = "in local declaration: var: unknown field dato" }, + { y = 4, x = 23, filename = "main.tl", msg = "invalid key 'dato' in record 'var' of type Boo" }, + }, result.type_errors) + end) + + it("can resolve a nominal with generics (regression test for #777)", function () + util.mock_io(finally, { + ["module.tl"] = [[ + local record module + record Foo + something: K + end + end + return module + ]], + ["main.tl"] = [[ + local module = require "module" + + local record Boo + field: MyFoo + end + + local type MyFoo = module.Foo + + local b: Boo = { field = { something = "hi" } } + local c: Boo = { field = { something = 123 } } + ]], + }) + local result, err = tl.process("main.tl") + + assert.same({}, result.syntax_errors) + assert.same({ + { y = 10, x = 52, filename = "main.tl", msg = "in record field: something: got integer, expected string" }, + }, result.type_errors) + end) + + it("catches unknown types", util.check_type_error([[ + local type MyType = UnknownType + ]], { + { msg = "unknown type UnknownType" } + })) + + it("nominal types can take type arguments", util.check([[ + local record Foo + item: R + end + + local type Foo2 = Foo + local type Bla = Foo + + local x: Bla = { item = 123 } + local y: Foo2 = { item = 123 } + ]])) + + it("declared as nominal types are aliases", util.check([[ + local record Foo + item: R + end + + local type Foo2 = Foo + local type FooNumber = Foo + + local x: FooNumber = { item = 123 } + local y: Foo2 = { item = 123 } + + local type Foo3 = Foo + local type Foo4 = Foo2 + + local zep: Foo2 = { item = "hello" } + local zip: Foo3 = zep + local zup: Foo4 = zip + ]])) + + it("nested types can be resolved as aliases", util.check([[ + local record Foo + enum LocalEnum + "loc" + end + + record Nested + x: {LocalEnum} + y: R + end + + item: R + end + + local type Nested = Foo.Nested + ]])) + + it("can require a module", function () + util.mock_io(finally, { + ["class.tl"] = [[ + local record Class + data: number + end + return Class + ]], + ["main.tl"] = [[ + local type Class = require("class") + local obj: Class = { data = 2 } + ]], + }) + local result, err = tl.process("main.tl") + + assert.same({}, result.syntax_errors) + assert.same({}, result.type_errors) + end) + + it("can require a module and type is usable", function () + util.mock_io(finally, { + ["class.tl"] = [[ + local record Class + data: number + end + return Class + ]], + ["main.tl"] = [[ + local type Class = require("class") + local obj: Class = { invalid = 2 } + ]], + }) + local result, err = tl.process("main.tl") + + assert.same({}, result.syntax_errors) + assert.same({ + { y = 2, x = 34, filename = "main.tl", msg = "in local declaration: obj: unknown field invalid" }, + }, result.type_errors) + end) + + it("can require a module and its globals are visible", function () + util.mock_io(finally, { + ["class.tl"] = [[ + global record Glob + hello: number + end + + local record Class + data: number + end + return Class + ]], + ["main.tl"] = [[ + local type Class = require("class") + local obj: Glob = { hello = 2 } + local obj2: Glob = { invalid = 2 } + ]], + }) + local result, err = tl.process("main.tl") + + assert.same({}, result.syntax_errors) + assert.same({ + { y = 3, x = 34, filename = "main.tl", msg = "in local declaration: obj2: unknown field invalid" }, + }, result.type_errors) + end) + + it("does not accept type arguments declared twice", util.check_syntax_error([[ + local type Foo = record + end + ]], { + { y = 1, msg = "cannot declare type arguments twice in type declaration" }, + })) + + it("propagates type arguments correctly", util.check_type_error([[ + local record module + record Foo + first: A + second: B + end + end + + -- note inverted arguments + local type MyFoo = module.Foo + + local record Boo + field: MyFoo + end + + local b: Boo = { field = { first = "first", second = 2 } } -- bad, not inverted! + local c: Boo = { field = { first = 1, second = "second" } } -- good, inverted! + ]], { + { y = 15, x = 42, msg = 'in record field: first: got string "first", expected integer' }, + { y = 15, x = 60, msg = 'in record field: second: got integer, expected string' }, + })) +end)