diff --git a/CHANGELOG.md b/CHANGELOG.md index d73ca39..7ab7e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # LuauPolyfills Changelog +## Unreleased + +* Strict luau type mode enabled on almost all polyfill files and tests. This may highlight latent type issues in code that consumes the polyfills, but was tested against apollo-client-lua (fixed merged), roact-alignment (no fixes needed), and jest-roblox (no fixes needed). +* Fix for `Array.from` to respect the `thisArg` argument when it is supplied. + ## 0.2.6 * add `has` method to `WeakMap` * add `expect` method to `Promise` diff --git a/foreman.toml b/foreman.toml index 95628d7..b61c980 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,5 +1,5 @@ [tools] rotrieve = { source = "roblox/rotriever", version = "=0.5.0-rc.4" } rojo = { source = "rojo-rbx/rojo", version = "6.2.0" } -selene = { source = "Kampfkarren/selene", version = "0.14" } -stylua = { source = "JohnnyMorganz/StyLua", version = "0.10.1" } +selene = { source = "Kampfkarren/selene", version = "0.15" } +stylua = { source = "JohnnyMorganz/StyLua", version = "0.11.1" } diff --git a/roblox-internal.toml b/roblox-internal.toml new file mode 100644 index 0000000..6145aff --- /dev/null +++ b/roblox-internal.toml @@ -0,0 +1,6 @@ +[[table.freeze.args]] +type = "table" + +[[table.isfrozen.args]] +type = "table" + diff --git a/rotriever.toml b/rotriever.toml index f3f3dc8..cb178a7 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -6,7 +6,7 @@ content_root = "src" files = ["*", "!**/__tests__/**"] [dev_dependencies] -JestGlobals = "github.com/roblox/jest-roblox@2.1.2" -TestEZ = "github.com/roblox/jest-roblox@2.0.1" +JestGlobals = "github.com/roblox/jest-roblox@2.2.1" +TestEZ = "github.com/roblox/jest-roblox@2.2.1" RegExp = "github.com/roblox/luau-regexp@0.1.3" Promise = "github.com/evaera/roblox-lua-promise@3.1.0" diff --git a/selene.toml b/selene.toml index 4e7811a..8e476aa 100644 --- a/selene.toml +++ b/selene.toml @@ -1,4 +1,4 @@ -std = "roblox+testez" +std = "roblox+testez+roblox-internal" [config] empty_if = { comments_count = true } diff --git a/src/Array/.robloxrc b/src/Array/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/Array/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/Array/__tests__/concat.spec.lua b/src/Array/__tests__/concat.spec.lua index ac6d34b..9f6fedd 100644 --- a/src/Array/__tests__/concat.spec.lua +++ b/src/Array/__tests__/concat.spec.lua @@ -1,6 +1,7 @@ -- Some tests are adapted from examples at: -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat return function() + type Array = { [number]: T } local Array = script.Parent.Parent local LuauPolyfill = Array.Parent local concat = require(Array.concat) @@ -44,16 +45,16 @@ return function() end) it("concatenates values to an array", function() - local letters = { "a", "b", "c" } + local letters: Array = { "a", "b", "c" } local alphaNumeric = concat(letters, 1, { 2, 3 }) - jestExpect(alphaNumeric).toEqual({ "a", "b", "c", 1, 2, 3 }) + jestExpect(alphaNumeric).toEqual({ "a", "b", "c", 1, 2, 3 } :: Array) end) it("concatenates nested arrays", function() local num1 = { { 1 } } - local num2 = { 2, { 3 } } + local num2: Array = { 2, { 3 } } local numbers = concat(num1, num2) - jestExpect(numbers).toEqual({ { 1 }, 2, { 3 } }) + jestExpect(numbers).toEqual({ { 1 }, 2, { 3 } } :: Array) end) if _G.__DEV__ then diff --git a/src/Array/__tests__/filter.spec.lua b/src/Array/__tests__/filter.spec.lua index 63226ad..2e2ac54 100644 --- a/src/Array/__tests__/filter.spec.lua +++ b/src/Array/__tests__/filter.spec.lua @@ -3,6 +3,7 @@ return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local filter = require(Array.filter) local isFinite = require(LuauPolyfill.Number.isFinite) @@ -43,10 +44,10 @@ return function() { id = 0 }, { id = 3 }, { id = 12.2 }, - {}, - { id = nil }, + {} :: any, + { id = nil } :: any, { id = 0 / 0 }, - { id = "undefined" }, + { id = "undefined" :: any }, } local invalidEntries = 0 @@ -97,10 +98,15 @@ return function() } local modifiedWords = filter(words, function(word, index, arr) - if arr[index + 1] == nil then - arr[index + 1] = "undefined" + -- Luau FIXME: I cannot get the narrowing to work here: TypeError: Value of type 'Array?' could be nil + if arr == nil or index == nil then + return false + end + + if (arr :: Array)[index :: number + 1] == nil then + (arr :: Array)[index :: number + 1] = "undefined" end - arr[index + 1] = arr[index + 1] .. " extra" + (arr :: Array)[index :: number + 1] = (arr :: Array)[index :: number + 1] .. " extra" return #word < 6 end) @@ -118,7 +124,10 @@ return function() } local modifiedWords = filter(words, function(word, index, arr) - table.insert(arr, "new") + if arr == nil then + return false + end + table.insert(arr :: Array, "new") return #word < 6 end) @@ -136,7 +145,10 @@ return function() } local modifiedWords = filter(words, function(word, index, arr) - table.remove(arr) + if arr == nil then + return false + end + table.remove(arr :: Array) return #word < 6 end) diff --git a/src/Array/__tests__/find.spec.lua b/src/Array/__tests__/find.spec.lua index 681015f..971bc1e 100644 --- a/src/Array/__tests__/find.spec.lua +++ b/src/Array/__tests__/find.spec.lua @@ -1,5 +1,6 @@ return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local find = require(Array.find) @@ -35,7 +36,8 @@ return function() local array = { "foo" } find(array, function(...) arguments = { ... } + return false end) - jestExpect(arguments).toEqual({ "foo", 1, array }) + jestExpect(arguments).toEqual({ "foo", 1, array } :: Array) end) end diff --git a/src/Array/__tests__/findIndex.spec.lua b/src/Array/__tests__/findIndex.spec.lua index a4a10db..94b0b91 100644 --- a/src/Array/__tests__/findIndex.spec.lua +++ b/src/Array/__tests__/findIndex.spec.lua @@ -1,5 +1,6 @@ return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local findIndex = require(Array.findIndex) @@ -35,8 +36,9 @@ return function() local array = { "foo" } findIndex(array, function(...) arguments = { ... } + return false end) - jestExpect(arguments).toEqual({ "foo", 1, array }) + jestExpect(arguments).toEqual({ "foo", 1, array } :: Array) end) -- the following tests were taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex @@ -51,7 +53,7 @@ return function() end) it("returns first prime element", function() - local function isPrime(num) + local function isPrime(num: number) for i = 2, num - 1 do if num % i == 0 then return false diff --git a/src/Array/__tests__/forEach.spec.lua b/src/Array/__tests__/forEach.spec.lua index 278cb92..43765a6 100644 --- a/src/Array/__tests__/forEach.spec.lua +++ b/src/Array/__tests__/forEach.spec.lua @@ -49,7 +49,7 @@ return function() it("forEach an array of numbers with a mixed field inserted", function() local mock = jest.fn() local numbers = { 1, 4, 9 } - numbers["NotANumber"] = "mixed" + numbers["NotANumber" :: any] = "mixed" :: any forEach(numbers, function(num) mock(num) end) @@ -104,7 +104,8 @@ return function() return result end - local nested = { 1, 2, 3, { 4, 5, { 6, 7 }, 8, 9 } } + -- Luau FIXME: Luau should realize this isn't an array in this single assingment: TypeError: Type '{number}' could not be converted into 'number' + local nested = { 1, 2, 3, { 4, 5, { 6, 7 } :: any, 8, 9 } :: any } jestExpect(flatten(nested)).toEqual({ 1, 2, 3, 4, 5, 6, 7, 8, 9 }) end) end diff --git a/src/Array/__tests__/from.spec.lua b/src/Array/__tests__/from.spec.lua index ed21b3a..08020af 100644 --- a/src/Array/__tests__/from.spec.lua +++ b/src/Array/__tests__/from.spec.lua @@ -2,6 +2,7 @@ -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local from = require(Array.from) local Set = require(LuauPolyfill).Set @@ -54,7 +55,8 @@ return function() local map = Map.new() map:set("key1", 31337) map:set("key2", 90210) - jestExpect(from(map)).toEqual({ { "key1", 31337 }, { "key2", 90210 } }) + -- Luau FIXME: Luau doesn't understand multi-typed arrays + jestExpect(from(map)).toEqual({ { "key1", 31337 :: any }, { "key2", 90210 :: any } }) end) it("returns an empty array from an empty Map", function() @@ -63,27 +65,37 @@ return function() describe("with mapping function", function() it("maps each character", function() - jestExpect(from("bar", function(character, index) + jestExpect(from("bar", function(character: string, index) return character .. index end)).toEqual({ "b1", "a2", "r3" }) end) it("maps each element of the array", function() jestExpect(from({ 10, 20 }, function(element, index) - return element + index + -- Luau FIXME: Luau should infer element type as number without annotation + return element :: number + index end)).toEqual({ 11, 22 }) end) + it("maps each element of the array with this arg", function() + local this = { state = 7 } + jestExpect(from({ 10, 20 }, function(self, element) + -- Luau FIXME: Luau should infer element type as number without annotation + return element :: number + self.state + end, this)).toEqual({ 17, 27 }) + end) + it("maps each element of the array from a Set", function() jestExpect(from(Set.new({ 1, 3 }), function(element, index) - return element + index + -- Luau FIXME: Luau should infer element type as number without annotation + return element :: number + index end)).toEqual({ 2, 5 }) end) it("maps each element of the array from a Map", function() local map = Map.new() map:set(-90210, 90210) - jestExpect(from(map, function(element, index) + jestExpect(from(map, function(element: Array, index) return element[1] + element[2] + index end)).toEqual({ -90210 + 90210 + 1 }) end) diff --git a/src/Array/__tests__/isArray.spec.lua b/src/Array/__tests__/isArray.spec.lua index 1f57669..dc665ff 100644 --- a/src/Array/__tests__/isArray.spec.lua +++ b/src/Array/__tests__/isArray.spec.lua @@ -2,6 +2,7 @@ -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local isArray = require(Array.isArray) @@ -57,6 +58,6 @@ return function() it("returns true for valid arrays", function() jestExpect(isArray({ "a", "b", "c" })).toEqual(true) jestExpect(isArray({ 1, 2, 3 })).toEqual(true) - jestExpect(isArray({ 1, "b", function() end })).toEqual(true) + jestExpect(isArray({ 1, "b", function() end } :: Array)).toEqual(true) end) end diff --git a/src/Array/__tests__/map.spec.lua b/src/Array/__tests__/map.spec.lua index da36cc5..91f90b4 100644 --- a/src/Array/__tests__/map.spec.lua +++ b/src/Array/__tests__/map.spec.lua @@ -51,7 +51,7 @@ return function() it("Mapping an array of numbers using a function containing an argument", function() local numbers = { 1, 4, 9 } - local doubles = map(numbers, function(num) + local doubles = map(numbers, function(num: number) return num * 2 end) jestExpect(doubles).toEqual({ 2, 8, 18 }) diff --git a/src/Array/__tests__/reduce.spec.lua b/src/Array/__tests__/reduce.spec.lua index 914fd3f..81cebd7 100644 --- a/src/Array/__tests__/reduce.spec.lua +++ b/src/Array/__tests__/reduce.spec.lua @@ -26,7 +26,9 @@ return function() it("throws if no initial value is provided and the array is empty", function() jestExpect(function() - reduce({}, function() end) + reduce({}, function() + return false + end) end).toThrow("reduce of empty array with no initial value") end) @@ -35,7 +37,9 @@ return function() -- invalid argument, so it needs to be cast to any local reduceAny: any = reduce jestExpect(function() - reduceAny(nil, function() end) + reduceAny(nil, function() + return false + end) end).toThrow() jestExpect(function() reduceAny({ 0, 1 }, nil) diff --git a/src/Array/__tests__/slice.spec.lua b/src/Array/__tests__/slice.spec.lua index d0b3e62..3e74511 100644 --- a/src/Array/__tests__/slice.spec.lua +++ b/src/Array/__tests__/slice.spec.lua @@ -1,9 +1,8 @@ ---!nocheck --- nocheck here since a test here purposefully violates the type check to test argument validation -- Tests adapted directly from examples at: -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice return function() local Array = script.Parent.Parent + type Array = { [number]: T } local LuauPolyfill = Array.Parent local slice = require(Array.slice) @@ -12,9 +11,9 @@ return function() local jestExpect = JestGlobals.expect it("Invalid argument", function() - -- Luau analysis correctly warns and prevents this abuse case! jestExpect(function() - slice(nil, 1) + -- Luau analysis correctly warns and prevents this abuse case, typecast to force it through + slice((nil :: any) :: Array, 1) end).toThrow() end) diff --git a/src/Array/__tests__/some.spec.lua b/src/Array/__tests__/some.spec.lua index 4262539..28f4938 100644 --- a/src/Array/__tests__/some.spec.lua +++ b/src/Array/__tests__/some.spec.lua @@ -41,7 +41,7 @@ return function() end) it("Converting any value to Boolean", function() - local truthy_values = { true, "true", 1 } + local truthy_values = { true, "true" :: any, 1 :: any } local getBoolean = function(value) return some(truthy_values, function(t) return t == value diff --git a/src/Array/concat.lua b/src/Array/concat.lua index 939ac5c..ac449b6 100644 --- a/src/Array/concat.lua +++ b/src/Array/concat.lua @@ -1,3 +1,4 @@ +--!strict local Array = script.Parent local isArray = require(Array.isArray) diff --git a/src/Array/every.lua b/src/Array/every.lua index 8d6fdd6..f2815a3 100644 --- a/src/Array/every.lua +++ b/src/Array/every.lua @@ -1,12 +1,12 @@ --!strict type Array = { [number]: T } -type callbackFn = (element: any, index: number?, array: Array?) -> boolean -type callbackFnWithThisArg = (thisArg: any, element: any, index: number?, array: Array?) -> boolean +type callbackFn = (element: T, index: number, array: Array?) -> boolean +type callbackFnWithThisArg = (self: U, element: T, index: number, array: Array?) -> boolean type Object = { [string]: any } -- Implements Javascript's `Array.prototype.every` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every -return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: Object?): boolean +return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: U?): boolean if typeof(t) ~= "table" then error(string.format("Array.every called on %s", typeof(t))) end @@ -23,9 +23,9 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thi if kValue ~= nil then if thisArg ~= nil then - testResult = (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) + testResult = (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) else - testResult = (callback :: callbackFn)(kValue, k, t) + testResult = (callback :: callbackFn)(kValue, k, t) end if not testResult then diff --git a/src/Array/filter.lua b/src/Array/filter.lua index e9df565..508f7b6 100644 --- a/src/Array/filter.lua +++ b/src/Array/filter.lua @@ -1,12 +1,12 @@ --!strict type Array = { [number]: T } -type callbackFn = (element: any, index: number?, array: Array?) -> boolean -type callbackFnWithThisArg = (thisArg: any, element: any, index: number?, array: Array?) -> boolean +type callbackFn = (element: T, index: number, array: Array?) -> boolean +type callbackFnWithThisArg = (thisArg: U, element: T, index: number, array: Array) -> boolean type Object = { [string]: any } -- Implements Javascript's `Array.prototype.filter` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter -return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: Object?): Array +return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: U?): Array if typeof(t) ~= "table" then error(string.format("Array.filter called on %s", typeof(t))) end @@ -22,7 +22,7 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thi for i = 1, len do local kValue = t[i] if kValue ~= nil then - if (callback :: callbackFn)(kValue, i, t) then + if (callback :: callbackFn)(kValue, i, t) then res[index] = kValue index += 1 end @@ -32,7 +32,7 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thi for i = 1, len do local kValue = t[i] if kValue ~= nil then - if (callback :: callbackFnWithThisArg)(thisArg, kValue, i, t) then + if (callback :: callbackFnWithThisArg)(thisArg, kValue, i, t) then res[index] = kValue index += 1 end diff --git a/src/Array/find.lua b/src/Array/find.lua index bcf511f..08884a1 100644 --- a/src/Array/find.lua +++ b/src/Array/find.lua @@ -1,7 +1,8 @@ +--!strict type Array = { [number]: T } -type PredicateFunction = (any, number, Array) -> boolean +type PredicateFunction = (value: T, index: number, array: Array) -> boolean -return function(array: Array, predicate: PredicateFunction): any | nil +return function(array: Array, predicate: PredicateFunction): T | nil for i = 1, #array do local element = array[i] if predicate(element, i, array) then diff --git a/src/Array/forEach.lua b/src/Array/forEach.lua index c04dfdc..7fee503 100644 --- a/src/Array/forEach.lua +++ b/src/Array/forEach.lua @@ -1,12 +1,12 @@ --!strict type Array = { [number]: T } -type callbackFn = (element: any, index: number?, array: Array?) -> () -type callbackFnWithThisArg = (thisArg: any, element: any, index: number?, array: Array?) -> () +type callbackFn = (element: T, index: number, array: Array?) -> () +type callbackFnWithThisArg = (thisArg: U, element: T, index: number, array: Array?) -> () type Object = { [string]: any } -- Implements Javascript's `Array.prototype.forEach` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach -return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: Object?): () +return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: U?): () if typeof(t) ~= "table" then error(string.format("Array.forEach called on %s", typeof(t))) end @@ -21,9 +21,9 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thi local kValue = t[k] if thisArg ~= nil then - (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) + (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) else - (callback :: callbackFn)(kValue, k, t) + (callback :: callbackFn)(kValue, k, t) end k += 1 diff --git a/src/Array/from.lua b/src/Array/from.lua index a7d91e4..7af0d61 100644 --- a/src/Array/from.lua +++ b/src/Array/from.lua @@ -1,3 +1,4 @@ +--!strict local Array = script.Parent type Array = { [number]: T } local isArray = require(Array.isArray) @@ -6,11 +7,11 @@ type Object = { [string]: any } local instanceof = require(LuauPolyfill.instanceof) local Set local Map -type Function = (...any) -> any +type mapFn = (element: T, index: number) -> U +type mapFnWithThisArg = (thisArg: any, element: T, index: number) -> U --- ROBLOX TODO: add function generics so item type is carried through to return value -- ROBLOX TODO: Object here is a stand-in for Map and Set, use those when we extract type-only files -return function(value: string | Array | Object, mapFn: Function | nil): Array +return function(value: string | Array | Object, mapFn: (mapFn | mapFnWithThisArg)?, thisArg: Object?): Array if not Set then Set = (require(LuauPolyfill).Set :: any) end @@ -28,18 +29,26 @@ return function(value: string | Array | Object, mapFn: Function | nil): Arr if valueType == "table" and isArray(value) then if mapFn then - for i = 1, #(value :: Array) do - array[i] = mapFn((value :: Array)[i], i) + for i = 1, #(value :: Array) do + if thisArg ~= nil then + array[i] = (mapFn :: mapFnWithThisArg)(thisArg, (value :: Array)[i], i) + else + array[i] = (mapFn :: mapFn)((value :: Array)[i], i) + end end else - for i = 1, #(value :: Array) do + for i = 1, #(value :: Array) do array[i] = (value :: Array)[i] end end elseif instanceof(value, Set) then if mapFn then for i, v in (value :: any):ipairs() do - array[i] = mapFn(v, i) + if thisArg ~= nil then + array[i] = (mapFn :: mapFnWithThisArg)(thisArg, v, i) + else + array[i] = (mapFn :: mapFn)(v, i) + end end else for i, v in (value :: any):ipairs() do @@ -49,7 +58,11 @@ return function(value: string | Array | Object, mapFn: Function | nil): Arr elseif instanceof(value, Map) then if mapFn then for i, v in (value :: any):ipairs() do - array[i] = mapFn(v, i) + if thisArg ~= nil then + array[i] = (mapFn :: mapFnWithThisArg)(thisArg, v, i) + else + array[i] = (mapFn :: mapFn)(v, i) + end end else for i, v in (value :: any):ipairs() do @@ -59,11 +72,15 @@ return function(value: string | Array | Object, mapFn: Function | nil): Arr elseif valueType == "string" then if mapFn then for i = 1, (value :: string):len() do - array[i] = mapFn((value :: string):sub(i, i), i) + if thisArg ~= nil then + array[i] = (mapFn :: mapFnWithThisArg)(thisArg, (value :: any):sub(i, i), i) + else + array[i] = (mapFn :: mapFn)((value :: any):sub(i, i), i) + end end else for i = 1, (value :: string):len() do - array[i] = (value :: string):sub(i, i) + array[i] = (value :: any):sub(i, i) end end end diff --git a/src/Array/includes.lua b/src/Array/includes.lua index 59b8bba..e1e8974 100644 --- a/src/Array/includes.lua +++ b/src/Array/includes.lua @@ -1,6 +1,7 @@ +--!strict local indexOf = require(script.Parent.indexOf) type Array = { [number]: T } -return function(array: Array, searchElement: any, fromIndex: number?): boolean +return function(array: Array, searchElement: any, fromIndex: number?): boolean return indexOf(array, searchElement, fromIndex) ~= -1 end diff --git a/src/Array/indexOf.lua b/src/Array/indexOf.lua index f1fd228..d33d9f5 100644 --- a/src/Array/indexOf.lua +++ b/src/Array/indexOf.lua @@ -1,6 +1,6 @@ ---!nolint LocalShadow +--!strict -type Array = { [number]: any } +type Array = { [number]: T } -- Implements equivalent functionality to JavaScript's `array.indexOf`, -- implementing the interface and behaviors defined at: @@ -8,18 +8,18 @@ type Array = { [number]: any } -- -- This implementation is loosely based on the one described in the polyfill -- source in the above link -return function(array: Array, searchElement: any, fromIndex: number?): number - local fromIndex: number = fromIndex or 1 +return function(array: Array, searchElement: any, fromIndex: number?): number + local fromIndex_ = fromIndex or 1 local length = #array -- In the JS impl, a negative fromIndex means we should use length - index; -- with Lua, of course, this means that 0 is still valid, but refers to the -- end of the array the way that '-1' would in JS - if fromIndex < 1 then - fromIndex = math.max(length - math.abs(fromIndex), 1) + if fromIndex_ < 1 then + fromIndex_ = math.max(length - math.abs(fromIndex_), 1) end - for i = fromIndex, length do + for i = fromIndex_, length do if array[i] == searchElement then return i end diff --git a/src/Array/init.lua b/src/Array/init.lua index 7535b43..87f4429 100644 --- a/src/Array/init.lua +++ b/src/Array/init.lua @@ -1,3 +1,4 @@ +--!strict export type Array = { [number]: T } return { diff --git a/src/Array/isArray.lua b/src/Array/isArray.lua index 8fc6f11..3a217aa 100644 --- a/src/Array/isArray.lua +++ b/src/Array/isArray.lua @@ -1,3 +1,4 @@ +--!strict return function(value: any): boolean if typeof(value) ~= "table" then return false diff --git a/src/Array/join.lua b/src/Array/join.lua index 7a48189..ddda244 100644 --- a/src/Array/join.lua +++ b/src/Array/join.lua @@ -1,3 +1,4 @@ +--!strict type Array = { [number]: T } local map = require(script.Parent.map) diff --git a/src/Array/map.lua b/src/Array/map.lua index 427e346..2e6a2b0 100644 --- a/src/Array/map.lua +++ b/src/Array/map.lua @@ -1,13 +1,13 @@ --!strict -type Array = { [number]: any } -type callbackFn = (element: any, index: number?, array: Array?) -> any -type callbackFnWithThisArg = (thisArg: any, element: any, index: number?, array: Array?) -> any +type Array = { [number]: T } +type callbackFn = (element: T, index: number, array: Array?) -> U +type callbackFnWithThisArg = (thisArg: V, element: T, index: number, array: Array?) -> U type Object = { [string]: any } -- Implements Javascript's `Array.prototype.map` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map -return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: Object?): Array +return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: V?): Array if typeof(t) ~= "table" then error(string.format("Array.map called on %s", typeof(t))) end @@ -26,9 +26,9 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: local mappedValue if thisArg ~= nil then - mappedValue = (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) + mappedValue = (callback :: callbackFnWithThisArg)(thisArg, kValue, k, t) else - mappedValue = (callback :: callbackFn)(kValue, k, t) + mappedValue = (callback :: callbackFn)(kValue, k, t) end A[k] = mappedValue diff --git a/src/Array/reduce.lua b/src/Array/reduce.lua index e3d4d4d..e3d7904 100644 --- a/src/Array/reduce.lua +++ b/src/Array/reduce.lua @@ -1,5 +1,4 @@ --!strict - type Array = { [number]: any } type Function = (any, any, number, any) -> any diff --git a/src/Array/reverse.lua b/src/Array/reverse.lua index 0a03630..73b5b85 100644 --- a/src/Array/reverse.lua +++ b/src/Array/reverse.lua @@ -1,3 +1,4 @@ +--!strict -- https://programming-idioms.org/idiom/19/reverse-a-list/1314/lua type Array = { [number]: any } function reverse(t: Array): Array diff --git a/src/Array/slice.lua b/src/Array/slice.lua index 6b099ba..2687b91 100644 --- a/src/Array/slice.lua +++ b/src/Array/slice.lua @@ -1,5 +1,4 @@ ---!nocheck --- CLI-37948: nocheck for now because narrowing fails +--!strict type Array = { [number]: T } @@ -11,29 +10,30 @@ return function(t: Array, start_idx: number?, end_idx: number?): Array end local length = #t - if start_idx == nil then - start_idx = 1 - end + local start_idx_ = start_idx or 1 + local end_idx_ if end_idx == nil or end_idx > length + 1 then - end_idx = length + 1 + end_idx_ = length + 1 + else + end_idx_ = end_idx end - if start_idx > length + 1 then + if start_idx_ > length + 1 then return {} end local slice = {} - if start_idx < 1 then - start_idx = math.max(length - math.abs(start_idx), 1) + if start_idx_ < 1 then + start_idx_ = math.max(length - math.abs(start_idx_), 1) end - if end_idx < 1 then - end_idx = math.max(length - math.abs(end_idx), 1) + if end_idx_ < 1 then + end_idx_ = math.max(length - math.abs(end_idx_), 1) end - local idx = start_idx + local idx = start_idx_ local i = 1 - while idx < end_idx do + while idx < end_idx_ do slice[i] = t[idx] idx = idx + 1 i = i + 1 diff --git a/src/Array/some.lua b/src/Array/some.lua index ccfb68d..3688e55 100644 --- a/src/Array/some.lua +++ b/src/Array/some.lua @@ -1,14 +1,13 @@ --!strict - -type Array = { [number]: any } +type Array = { [number]: T } -- note: JS version can return anything that's truthy, but that won't work for us since Lua deviates (0 is truthy) -type callbackFn = (element: any, index: number?, array: Array?) -> boolean -type callbackFnWithThisArg = (thisArg: any, element: any, index: number?, array: Array?) -> boolean +type callbackFn = (element: T, index: number, array: Array) -> boolean +type callbackFnWithThisArg = (thisArg: U, element: T, index: number, array: Array) -> boolean type Object = { [string]: any } -- Implements Javascript's `Array.prototype.map` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some -return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: Object?) +return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: U?) if typeof(t) ~= "table" then error(string.format("Array.some called on %s", typeof(t))) end @@ -18,11 +17,11 @@ return function(t: Array, callback: callbackFn | callbackFnWithThisArg, thisArg: for i, value in ipairs(t) do if thisArg ~= nil then - if value ~= nil and (callback :: callbackFnWithThisArg)(thisArg, value, i, t) then + if value ~= nil and (callback :: callbackFnWithThisArg)(thisArg, value, i, t) then return true end else - if value ~= nil and (callback :: callbackFn)(value, i, t) then + if value ~= nil and (callback :: callbackFn)(value, i, t) then return true end end diff --git a/src/Array/sort.lua b/src/Array/sort.lua index b8f9f80..eb38244 100644 --- a/src/Array/sort.lua +++ b/src/Array/sort.lua @@ -1,17 +1,19 @@ +--!strict type Array = { [number]: T } type Comparable = (any, any) -> number -local function defaultSort(a, b) +local defaultSort = function(a: any, b: any): boolean return tostring(a) < tostring(b) end local function sort(array: Array, compare: Comparable?) + -- wrapperCompare interprets compare return value to be compatible with Lua's table.sort local wrappedCompare = defaultSort if compare ~= nil then - if typeof(compare) ~= "function" then + if typeof(compare :: any) ~= "function" then error("invalid argument to Array.sort: compareFunction must be a function") end wrappedCompare = function(a, b) - local result = (compare :: Comparable)(a, b) + local result = compare(a, b) if typeof(result) ~= "number" then -- deviation: throw an error because -- it's not clearly defined what is diff --git a/src/Array/splice.lua b/src/Array/splice.lua index d6d7ff9..066ede3 100644 --- a/src/Array/splice.lua +++ b/src/Array/splice.lua @@ -1,5 +1,4 @@ ---!nolint LocalShadow - +--!strict type Array = { [number]: any } -- Implements equivalent functionality to JavaScript's `array.splice`, including @@ -26,9 +25,9 @@ return function(array: Array, start: number, deleteCount: number?, ...): Array local deletedItems = {} -- If no deleteCount was provided, we want to delete the rest of the -- array starting with `start` - local deleteCount: number = deleteCount or length - if deleteCount > 0 then - local lastIndex = math.min(length, start + math.max(0, deleteCount - 1)) + local deleteCount_: number = deleteCount or length + if deleteCount_ > 0 then + local lastIndex = math.min(length, start + math.max(0, deleteCount_ - 1)) for i = start, lastIndex do local deleted = table.remove(array, start) diff --git a/src/Boolean/.robloxrc b/src/Boolean/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/Boolean/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/Boolean/init.lua b/src/Boolean/init.lua index cc3ff9d..afea45b 100644 --- a/src/Boolean/init.lua +++ b/src/Boolean/init.lua @@ -1,3 +1,4 @@ +--!strict return { toJSBoolean = require(script.toJSBoolean), } diff --git a/src/Error/.robloxrc b/src/Error/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/Error/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/Error/__tests__/Error.spec.lua b/src/Error/__tests__/Error.spec.lua index 1f7c5f7..8a19e80 100644 --- a/src/Error/__tests__/Error.spec.lua +++ b/src/Error/__tests__/Error.spec.lua @@ -1,5 +1,3 @@ ---!nocheck --- FIXME: Luau detects __call metamethod, but still things said object isn't callable: https://jira.rbx.com/browse/CLI-40294 return function() local ErrorModule = script.Parent.Parent local Error = require(ErrorModule) @@ -24,19 +22,19 @@ return function() end) it("accepts a message value as an argument", function() - local err: Error = Error("Some message") + local err = Error("Some message") jestExpect(err.message).toEqual("Some message") end) it("defaults the `name` field to 'Error'", function() - local err: Error = Error("") + local err = Error("") jestExpect(err.name).toEqual("Error") end) it("gets passed through the `error` builtin properly", function() - local err: Error = Error("Throwing an error") + local err = Error("Throwing an error") local ok, result = pcall(function() error(err) end) @@ -46,7 +44,7 @@ return function() end) it("checks that Error is a class according to our inheritance standard", function() - local err: Error = Error("Test") + local err = Error("Test") jestExpect(instanceof(err, Error)).toEqual(true) end) @@ -85,8 +83,8 @@ return function() end) it("checks Error stack field", function() - local err: Error = Error("test stack for Error()") - local err2: Error = Error.new("test stack for Error.new()") + local err = Error("test stack for Error()") + local err2 = Error.new("test stack for Error.new()") local topLineRegExp = RegExp("^.*Error.__tests__\\.Error\\.spec:\\d+") diff --git a/src/Error/init.lua b/src/Error/init.lua index 6682ca2..d40545e 100644 --- a/src/Error/init.lua +++ b/src/Error/init.lua @@ -1,4 +1,4 @@ ---!nocheck +--!strict export type Error = { name: string, message: string, stack: string? } local Error = {} @@ -6,15 +6,18 @@ local Error = {} local DEFAULT_NAME = "Error" Error.__index = Error Error.__tostring = function(self) - return getmetatable(Error).__tostring(self) + -- Luau FIXME: I can't cast to Error or Object here: Type 'Object' could not be converted into '{ @metatable *unknown*, {| |} }' + return getmetatable(Error :: any).__tostring(self) end -function Error.new(message: string?) - return setmetatable({ - name = DEFAULT_NAME, - message = message or "", - stack = debug.traceback(nil, 2), - }, Error) +function Error.new(message: string?): Error + return ( + setmetatable({ + name = DEFAULT_NAME, + message = message or "", + stack = debug.traceback(nil, 2), + }, Error) :: any + ) :: Error end return setmetatable(Error, { diff --git a/src/Map.lua b/src/Map.lua index 4378d7e..bc1c535 100644 --- a/src/Map.lua +++ b/src/Map.lua @@ -1,3 +1,4 @@ +--!strict local LuauPolyfill = script.Parent local Array = require(LuauPolyfill.Array) @@ -13,18 +14,20 @@ local Map = {} export type Map = { size: number, -- method definitions - set: (Map, T, V) -> Map, - get: (Map, T) -> V, - clear: (Map) -> (), - delete: (Map, T) -> boolean, - has: (Map, T) -> boolean, - keys: (Map) -> Array, - values: (Map) -> Array, - entries: (Map) -> Array>, - ipairs: (Map) -> any, + set: (self: Map, T, V) -> Map, + get: (self: Map, T) -> V, + clear: (self: Map) -> (), + delete: (self: Map, T) -> boolean, + has: (self: Map, T) -> boolean, + keys: (self: Map) -> Array, + values: (self: Map) -> Array, + entries: (self: Map) -> Array>, + ipairs: (self: Map) -> any, + _map: { [T]: V }, + _array: { [number]: T }, } -function Map.new(iterable): Map +function Map.new(iterable: Array?): Map local array = {} local map = {} if iterable ~= nil then @@ -58,7 +61,8 @@ end function Map:set(key, value) -- preserve initial insertion order if self._map[key] == nil then - self.size += 1 + -- Luau FIXME: analyze should know self is Map which includes size as a number + self.size = self.size :: number + 1 table.insert(self._array, key) end -- always update value @@ -81,7 +85,8 @@ function Map:delete(key): boolean if self._map[key] == nil then return false end - self.size -= 1 + -- Luau FIXME: analyze should know self is Map which includes size as a number + self.size = self.size :: number - 1 self._map[key] = nil local index = table.find(self._array, key) if index then diff --git a/src/Math/.robloxrc b/src/Math/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/Math/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/Math/clz32.lua b/src/Math/clz32.lua index adb47ed..88e8152 100644 --- a/src/Math/clz32.lua +++ b/src/Math/clz32.lua @@ -1,3 +1,4 @@ +--!strict local rshift = bit32.rshift local log = math.log local floor = math.floor diff --git a/src/Math/init.lua b/src/Math/init.lua index 36d73bf..257662a 100644 --- a/src/Math/init.lua +++ b/src/Math/init.lua @@ -1,3 +1,4 @@ +--!strict return { clz32 = require(script.clz32), } diff --git a/src/Number/.robloxrc b/src/Number/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/Number/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/Number/MAX_SAFE_INTEGER.lua b/src/Number/MAX_SAFE_INTEGER.lua index 4c956b5..21e2741 100644 --- a/src/Number/MAX_SAFE_INTEGER.lua +++ b/src/Number/MAX_SAFE_INTEGER.lua @@ -1,2 +1,3 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER return 9007199254740991 diff --git a/src/Number/MIN_SAFE_INTEGER.lua b/src/Number/MIN_SAFE_INTEGER.lua index 9a287b4..08507d2 100644 --- a/src/Number/MIN_SAFE_INTEGER.lua +++ b/src/Number/MIN_SAFE_INTEGER.lua @@ -1,2 +1,3 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER return -9007199254740991 diff --git a/src/Number/__tests__/isFinite.spec.lua b/src/Number/__tests__/isFinite.spec.lua index 62ce2d1..63e0b44 100644 --- a/src/Number/__tests__/isFinite.spec.lua +++ b/src/Number/__tests__/isFinite.spec.lua @@ -14,7 +14,7 @@ return function() math.huge, 0 / 0, -math.huge, - "0", + "0" :: any, } for _, value in ipairs(trueValues) do diff --git a/src/Number/__tests__/isSafeInteger.spec.lua b/src/Number/__tests__/isSafeInteger.spec.lua index bad5ea8..1c9e502 100644 --- a/src/Number/__tests__/isSafeInteger.spec.lua +++ b/src/Number/__tests__/isSafeInteger.spec.lua @@ -32,7 +32,7 @@ return function() end) it("returns false when given '3'", function() - jestExpect(isSafeInteger("3")).toEqual(false) + jestExpect(isSafeInteger("3" :: any)).toEqual(false) end) it("returns false when given 3.1", function() diff --git a/src/Number/__tests__/toExponential.spec.lua b/src/Number/__tests__/toExponential.spec.lua index a73d5ce..c1b2dff 100644 --- a/src/Number/__tests__/toExponential.spec.lua +++ b/src/Number/__tests__/toExponential.spec.lua @@ -9,11 +9,11 @@ return function() describe("returns nil for invalid input", function() it("toExponential(nil)", function() - jestExpect(toExponential(nil)).toEqual(nil) + jestExpect(toExponential(nil :: any)).toEqual(nil) end) it("toExponential('abcd')", function() - jestExpect(toExponential("abcd")).toEqual(nil) + jestExpect(toExponential("abcd" :: any)).toEqual(nil) end) end) @@ -32,7 +32,7 @@ return function() it("toExponential(77.1234, 'abcd')", function() jestExpect(function() - toExponential(77.1234, "abcd") + toExponential(77.1234, "abcd" :: any) end).toThrow() end) end) diff --git a/src/Number/init.lua b/src/Number/init.lua index 91bc0ff..1371726 100644 --- a/src/Number/init.lua +++ b/src/Number/init.lua @@ -1,3 +1,4 @@ +--!strict return { isFinite = require(script.isFinite), isInteger = require(script.isInteger), diff --git a/src/Number/isFinite.lua b/src/Number/isFinite.lua index 7934b52..91f2b1c 100644 --- a/src/Number/isFinite.lua +++ b/src/Number/isFinite.lua @@ -1,3 +1,4 @@ +--!strict return function(value) return typeof(value) == "number" and value == value and value ~= math.huge and value ~= -math.huge end diff --git a/src/Number/isInteger.lua b/src/Number/isInteger.lua index 4c517c4..9a49851 100644 --- a/src/Number/isInteger.lua +++ b/src/Number/isInteger.lua @@ -1,3 +1,4 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger return function(value) return type(value) == "number" and value ~= math.huge and value == math.floor(value) diff --git a/src/Number/isNaN.lua b/src/Number/isNaN.lua index cb2232f..22176f5 100644 --- a/src/Number/isNaN.lua +++ b/src/Number/isNaN.lua @@ -1,3 +1,4 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN return function(value) return type(value) == "number" and value ~= value diff --git a/src/Number/isSafeInteger.lua b/src/Number/isSafeInteger.lua index c6489a5..e3a3150 100644 --- a/src/Number/isSafeInteger.lua +++ b/src/Number/isSafeInteger.lua @@ -1,3 +1,4 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger local isInteger = require(script.Parent.isInteger) local MAX_SAFE_INTEGER = require(script.Parent.MAX_SAFE_INTEGER) diff --git a/src/Number/toExponential.lua b/src/Number/toExponential.lua index 40c66d1..ef8abc0 100644 --- a/src/Number/toExponential.lua +++ b/src/Number/toExponential.lua @@ -1,5 +1,6 @@ +--!strict -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential -return function(value, fractionDigits): string | nil +return function(value: string | number, fractionDigits: number?): string | nil local num = value if typeof(value) == "string" then num = tonumber(value) diff --git a/src/Object/None.lua b/src/Object/None.lua index b9d285a..ead51df 100644 --- a/src/Object/None.lua +++ b/src/Object/None.lua @@ -1,7 +1,9 @@ +--!nonstrict -- Marker used to specify that the value is nothing, because nil cannot be -- stored in tables. local None = newproxy(true) -getmetatable(None).__tostring = function() +local mt = getmetatable(None) +mt.__tostring = function() return "Object.None" end diff --git a/src/Object/__tests__/assign.spec.lua b/src/Object/__tests__/assign.spec.lua index 6dfe75a..2922f84 100644 --- a/src/Object/__tests__/assign.spec.lua +++ b/src/Object/__tests__/assign.spec.lua @@ -1,4 +1,3 @@ ---!nocheck return function() local Object = script.Parent.Parent local None = require(Object.None) @@ -34,9 +33,7 @@ return function() assign(target, source1, source2) - jestExpect(target.a).toEqual(5) - jestExpect(target.b).toEqual(source2.b) - jestExpect(target.c).toEqual(source1.c) + jestExpect(target).toEqual({ a = 5, b = source2.b, c = source1.c }) end) it("should remove keys if specified as None", function() @@ -85,7 +82,6 @@ return function() assign(target, nil, true, 1, source1) - jestExpect(target.foo).toEqual(2) - jestExpect(target.bar).toEqual(1) + jestExpect(target).toEqual({ foo = 2, bar = 1 }) end) end diff --git a/src/Object/__tests__/entries.spec.lua b/src/Object/__tests__/entries.spec.lua index ed139a0..5a7ba81 100644 --- a/src/Object/__tests__/entries.spec.lua +++ b/src/Object/__tests__/entries.spec.lua @@ -1,6 +1,9 @@ +--!nonstrict -- tests based on the examples provided on MDN web docs: -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries return function() + type Object = { [string]: any } + type Array = { [number]: T } local Object = script.Parent.Parent local entries = require(Object.entries) @@ -14,13 +17,14 @@ return function() end) it("returns an array of key-value pairs", function() - local result = entries({ + local result: Array> = entries({ foo = "bar", baz = 42, }) table.sort(result, function(a, b) return a[1] < b[1] end) + -- Luau FIXME: Luau should see result as Array>, given object is [string]: any, but it sees it as Array despite all the manual annotation jestExpect(result).toEqual({ { "baz", 42 }, { "foo", "bar" }, @@ -31,9 +35,9 @@ return function() -- To not risk making the function significantly slower, this behavior is -- not implemented itSKIP("returns an array with the stringified indexes given an array", function() - jestExpect(entries({ true, false, "foo" })).toEqual({ - { "1", true }, - { "2", false }, + jestExpect(entries({ true, false, "foo" :: any })).toEqual({ + { "1", true :: any }, + { "2", false :: any }, { "3", "foo" }, }) end) @@ -56,7 +60,8 @@ return function() it("throws given a nil value", function() jestExpect(function() - entries(nil) + -- re-cast since typechecking would disallow this abuse case + entries((nil :: any) :: Object) end).toThrow("cannot get entries from a nil value") end) end diff --git a/src/Object/assign.lua b/src/Object/assign.lua index 2ef2aba..a293066 100644 --- a/src/Object/assign.lua +++ b/src/Object/assign.lua @@ -1,3 +1,4 @@ +--!strict local None = require(script.Parent.None) --[[ @@ -6,7 +7,8 @@ local None = require(script.Parent.None) This function is identical in functionality to JavaScript's Object.assign. ]] -local function assign(target: { [any]: any }, ...) +-- Luau TODO: no way to strongly type this, it can't do intersections of type packs: (T, ...: ...U): T & ...U +local function assign(target: { [any]: any }, ...): any for index = 1, select("#", ...) do local source = select(index, ...) diff --git a/src/Object/entries.lua b/src/Object/entries.lua index 37e5f29..45d3ebe 100644 --- a/src/Object/entries.lua +++ b/src/Object/entries.lua @@ -1,17 +1,18 @@ +--!strict type Object = { [string]: any } local Array = require(script.Parent.Parent.Array) type Array = Array.Array +type Tuple = Array -return function(value: string | Object | Array | nil): Array - if value == nil then - error("cannot get entries from a nil value") - end +return function(value: string | Object | Array): Array + assert(value :: any ~= nil, "cannot get entries from a nil value") local valueType = typeof(value) - local entries = {} + local entries: Array> = {} if valueType == "table" then for key, keyValue in pairs(value :: Object) do - table.insert(entries, { key, keyValue }) + -- Luau FIXME: Luau should see entries as Array, given object is [string]: any, but it sees it as Array> despite all the manual annotation + table.insert(entries, { key :: string, keyValue :: any }) end elseif valueType == "string" then for i = 1, string.len(value :: string) do diff --git a/src/Object/freeze.lua b/src/Object/freeze.lua index 8ac5cff..fcd7768 100644 --- a/src/Object/freeze.lua +++ b/src/Object/freeze.lua @@ -1,3 +1,4 @@ +--!strict local preventExtensions = require(script.Parent.preventExtensions) local function freeze(t) diff --git a/src/Object/init.lua b/src/Object/init.lua index f88d3a0..6b17457 100644 --- a/src/Object/init.lua +++ b/src/Object/init.lua @@ -1,3 +1,4 @@ +--!strict export type Object = { [string]: any? } return { diff --git a/src/Object/is.lua b/src/Object/is.lua index 8c9ff16..3bd2e18 100644 --- a/src/Object/is.lua +++ b/src/Object/is.lua @@ -1,5 +1,3 @@ ---!strict - -- Implements Javascript's `Object.is` as defined below -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is return function(value1: any, value2: any): boolean diff --git a/src/Object/preventExtensions.lua b/src/Object/preventExtensions.lua index 688bfd2..1623c91 100644 --- a/src/Object/preventExtensions.lua +++ b/src/Object/preventExtensions.lua @@ -1,17 +1,21 @@ +--!strict +type Object = { [string]: any } -- FIXME: This should be updated to be closer to the actual -- `Object.preventExtensions` functionality in JS. This requires additional -- support from the VM -local function preventExtensions(t) +local function preventExtensions(t: Object): T local name = tostring(t) - return setmetatable(t, { - __newindex = function(self, key, value) - local message = ("%q (%s) is not a valid member of %s"):format(tostring(key), typeof(key), name) + return ( + setmetatable(t, { + __newindex = function(self, key, value) + local message = ("%q (%s) is not a valid member of %s"):format(tostring(key), typeof(key), name) - error(message, 2) - end, - __metatable = false, - }) + error(message, 2) + end, + __metatable = false, + }) :: any + ) :: T end return preventExtensions diff --git a/src/Object/seal.lua b/src/Object/seal.lua index 2fb8bca..84889c4 100644 --- a/src/Object/seal.lua +++ b/src/Object/seal.lua @@ -1,12 +1,8 @@ -local preventExtensions = require(script.Parent.preventExtensions) +--!strict +type Object = { [string]: any } -local function seal(t) - -- FIXME: We don't have needed VM support to mimic the functionality of - -- seal, so we approximate with preventExtensions Seal should also support: - -- * Reassigning to a value that was set to nil - -- * Preventing removal of a field; this is hard to define given lua's - -- understanding of 'nil' and table membership - return preventExtensions(t) +local function seal(t: Object): T + return (table.freeze(t) :: any) :: T end return seal diff --git a/src/Object/values.lua b/src/Object/values.lua index 96c16a7..7789f31 100644 --- a/src/Object/values.lua +++ b/src/Object/values.lua @@ -1,3 +1,4 @@ +--!strict type Table = { [any]: any } type Array = { [number]: T } diff --git a/src/Promise.lua b/src/Promise.lua index d6d687e..aabdf59 100644 --- a/src/Promise.lua +++ b/src/Promise.lua @@ -1,17 +1,18 @@ +--!strict -- this maps onto community promise libraries which won't support Luau, so we inline export type PromiseLike = { andThen: ( - PromiseLike, -- self - ((T) -> ...(nil | T | PromiseLike))?, -- resolve - ((any) -> ...(nil | T | PromiseLike))? -- reject + self: PromiseLike, + resolve: ((T) -> ...(nil | T | PromiseLike))?, + reject: ((any) -> ...(nil | T | PromiseLike))? ) -> PromiseLike, } export type Promise = { andThen: ( - Promise, -- self - ((T) -> ...(nil | T | PromiseLike))?, -- resolve - ((any) -> ...(nil | T | PromiseLike))? -- reject + self: Promise, + resolve: ((T) -> ...(nil | T | PromiseLike))?, + reject: ((any) -> ...(nil | T | PromiseLike))? ) -> Promise, catch: (Promise, ((any) -> ...(nil | T | PromiseLike))) -> Promise, diff --git a/src/Set.lua b/src/Set.lua index 6875793..b97a3c2 100644 --- a/src/Set.lua +++ b/src/Set.lua @@ -1,3 +1,4 @@ +--!nonstrict local LuauPolyfill = script.Parent local Array = require(LuauPolyfill.Array) type Array = Array.Array @@ -72,7 +73,8 @@ end function Set:add(value) if not self._map[value] then - self.size += 1 + -- Luau FIXME: analyze should know self is Map which includes size as a number + self.size = self.size :: number + 1 self._map[value] = true table.insert(self._array, value) end @@ -89,7 +91,8 @@ function Set:delete(value): boolean if not self._map[value] then return false end - self.size -= 1 + -- Luau FIXME: analyze should know self is Map which includes size as a number + self.size = self.size :: number - 1 self._map[value] = nil local index = table.find(self._array, value) if index then diff --git a/src/String/.robloxrc b/src/String/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/String/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/String/__tests__/findOr.spec.lua b/src/String/__tests__/findOr.spec.lua index a755c74..79ad8e4 100644 --- a/src/String/__tests__/findOr.spec.lua +++ b/src/String/__tests__/findOr.spec.lua @@ -21,8 +21,7 @@ return function() index = 2, match = "b", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns 2nd instance of matched element after start position", function() @@ -33,8 +32,7 @@ return function() index = 4, match = "b", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns if any items match", function() @@ -45,8 +43,7 @@ return function() index = 2, match = "rn", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns 2nd instance if any items match after start position", function() @@ -57,8 +54,7 @@ return function() index = 5, match = "r", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns matched multiple characters", function() @@ -69,8 +65,7 @@ return function() index = 2, match = "bbb", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns matched element when multi-byte character present in the source string", function() @@ -81,8 +76,7 @@ return function() index = 3, match = "b", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) it("returns matched element after init index when multi-byte character present in the source string", function() @@ -93,7 +87,6 @@ return function() index = 5, match = "b", } - jestExpect(actual.index).toEqual(expected.index) - jestExpect(actual.match).toEqual(expected.match) + jestExpect(actual).toEqual(expected) end) end diff --git a/src/String/charCodeAt.lua b/src/String/charCodeAt.lua index e9c134c..4ef1616 100644 --- a/src/String/charCodeAt.lua +++ b/src/String/charCodeAt.lua @@ -1,3 +1,4 @@ +--!strict local String = script.Parent local LuauPolyfill = String.Parent local Number = require(LuauPolyfill.Number) @@ -11,7 +12,9 @@ return function(str: string, index: number): number index = 1 end - if index > utf8.len(str) or index < 1 then + local strLen, invalidBytePosition = utf8.len(str) + assert(strLen ~= nil, ("string `%s` has an invalid byte at position %s"):format(str, tostring(invalidBytePosition))) + if index > strLen or index < 1 then return NaN end diff --git a/src/String/endsWith.lua b/src/String/endsWith.lua index c36e64f..9b81ebf 100644 --- a/src/String/endsWith.lua +++ b/src/String/endsWith.lua @@ -1,3 +1,4 @@ +--!strict local function endsWith(value: string, substring: string, optionalLength: number?): boolean local substringLength = substring:len() if substringLength == 0 then diff --git a/src/String/findOr.lua b/src/String/findOr.lua index a154e76..100e900 100644 --- a/src/String/findOr.lua +++ b/src/String/findOr.lua @@ -1,4 +1,10 @@ -local function findOr(str: string, patternTable: { string }, initIndex: number?) +--!strict +type Match = { + index: number, + match: string, +} + +local function findOr(str: string, patternTable: { string }, initIndex: number?): Match | nil -- loop through all options in patern patternTable local init = utf8.offset(str, initIndex or 1) @@ -8,10 +14,10 @@ local function findOr(str: string, patternTable: { string }, initIndex: number?) if iStart then local prefix = string.sub(str, 1, iStart - 1) local prefixEnd, invalidBytePosition = utf8.len(prefix) - if not prefixEnd then - error(("string `%s` has an invalid byte at position %d"):format(prefix, invalidBytePosition)) + if prefixEnd == nil then + error(("string `%s` has an invalid byte at position %s"):format(prefix, tostring(invalidBytePosition))) end - local iStartIndex = prefixEnd + 1 + local iStartIndex = prefixEnd :: number + 1 local match = { index = iStartIndex, match = string.sub(str, iStart, iEnd), diff --git a/src/String/init.lua b/src/String/init.lua index be76782..d670373 100644 --- a/src/String/init.lua +++ b/src/String/init.lua @@ -1,3 +1,4 @@ +--!strict return { charCodeAt = require(script.charCodeAt), endsWith = require(script.endsWith), diff --git a/src/String/lastIndexOf.lua b/src/String/lastIndexOf.lua index 5fd9924..ee23345 100644 --- a/src/String/lastIndexOf.lua +++ b/src/String/lastIndexOf.lua @@ -1,7 +1,10 @@ +--!strict local function lastIndexOf(str: string, searchValue: string, fromIndex: number?): number local strLength = string.len(str) - local calculatedFromIndex = fromIndex - if not fromIndex then + local calculatedFromIndex + if fromIndex then + calculatedFromIndex = fromIndex + else calculatedFromIndex = strLength end if fromIndex and fromIndex < 1 then @@ -11,20 +14,24 @@ local function lastIndexOf(str: string, searchValue: string, fromIndex: number?) calculatedFromIndex = strLength end if searchValue == "" then - return calculatedFromIndex + -- FIXME: Luau DFA doesn't understand that + return calculatedFromIndex :: number end local lastFoundStartIndex, foundStartIndex - local foundEndIndex = 0 + -- Luau FIXME: Luau doesn't look beyond assignment for type, it should infer number? from loop bound + local foundEndIndex: number? = 0 repeat lastFoundStartIndex = foundStartIndex - foundStartIndex, foundEndIndex = string.find(str, searchValue, foundEndIndex + 1, true) + -- Luau FIXME: DFA doesn't understand until clause means foundEndIndex is never nil within loop + foundStartIndex, foundEndIndex = string.find(str, searchValue, foundEndIndex :: number + 1, true) until foundStartIndex == nil or foundStartIndex > calculatedFromIndex if lastFoundStartIndex == nil then return -1 end - return lastFoundStartIndex + -- Luau FIXME: Luau should see the predicate above and known the line below can only be a number + return lastFoundStartIndex :: number end return lastIndexOf diff --git a/src/String/slice.lua b/src/String/slice.lua index 86b2cfd..a85f088 100644 --- a/src/String/slice.lua +++ b/src/String/slice.lua @@ -1,5 +1,7 @@ +--!strict local function slice(str: string, startIndexStr: string | number, lastIndexStr: (string | number)?): string - local strLen = utf8.len(str) + local strLen, invalidBytePosition = utf8.len(str) + assert(strLen ~= nil, ("string `%s` has an invalid byte at position %s"):format(str, tostring(invalidBytePosition))) local startIndex = tonumber(startIndexStr) assert(typeof(startIndex) == "number", "startIndexStr should be a number") diff --git a/src/String/split.lua b/src/String/split.lua index 8517f05..d5b923f 100644 --- a/src/String/split.lua +++ b/src/String/split.lua @@ -1,3 +1,4 @@ +--!strict local findOr = require(script.Parent.findOr) local slice = require(script.Parent.slice) @@ -22,21 +23,32 @@ local function split(str: string, pattern: Pattern?): Array local init = 1 local result = {} local lastMatch + local strLen, invalidBytePosition = utf8.len(str) + assert(strLen ~= nil, ("string `%s` has an invalid byte at position %s"):format(str, tostring(invalidBytePosition))) + repeat local match = findOr(str, patternList, init) if match ~= nil then table.insert(result, slice(str, init, match.index)) - init = match.index + utf8.len(match.match) + local matchLength = utf8.len(match.match) + -- Luau FIXME? Luau doesn't understand that str has already been shown to be valid utf8 on line 26 and therefore won't be nil + init = match.index + matchLength :: number else table.insert(result, slice(str, init, nil)) end if match ~= nil then lastMatch = match end - until match == nil or init > utf8.len(str) - local strLen = utf8.len(str) - if lastMatch ~= nil and lastMatch.index + utf8.len(lastMatch.match) == strLen + 1 then - table.insert(result, "") + until match == nil or init > strLen + if lastMatch ~= nil then + local lastMatchLength, invalidBytePosition_ = utf8.len(lastMatch.match) + assert( + lastMatchLength ~= nil, + ("string `%s` has an invalid byte at position %s"):format(lastMatch.match, tostring(invalidBytePosition_)) + ) + if lastMatch.index + lastMatchLength == strLen + 1 then + table.insert(result, "") + end end return result end diff --git a/src/String/startsWith.lua b/src/String/startsWith.lua index 9d2301e..d771257 100644 --- a/src/String/startsWith.lua +++ b/src/String/startsWith.lua @@ -1,16 +1,20 @@ +--!strict local function startsWith(value: string, substring: string, position: number?): boolean - if substring:len() == 0 then + if string.len(substring) == 0 then return true end + -- Luau FIXME: we have to use a tmp variable, as Luau doesn't understand the logic below narrow position to `number` + local position_ if position == nil or position < 1 then - position = 1 + position_ = 1 + else + position_ = position end - if position > value:len() then + + if position_ > string.len(value) then return false end - -- FIXME: workaround for Luau issue https://jira.rbx.com/browse/CLI-40887 - local _position: number = position or 1 - return value:find(substring, _position, true) == _position + return value:find(substring, position_, true) == position_ end return startsWith diff --git a/src/String/substr.lua b/src/String/substr.lua index dccb6e8..b12499e 100644 --- a/src/String/substr.lua +++ b/src/String/substr.lua @@ -1,15 +1,7 @@ +--!strict return function(s: string, startIndex: number, numberOfCharacters: number?): string - if numberOfCharacters and (numberOfCharacters :: number) <= 0 then + if numberOfCharacters and numberOfCharacters <= 0 then return "" end - return string.sub( - s, - startIndex, - (function() - if not numberOfCharacters then - return numberOfCharacters - end - return startIndex + (numberOfCharacters :: number) - 1 - end)() - ) + return string.sub(s, startIndex, numberOfCharacters and startIndex + numberOfCharacters - 1 or nil) end diff --git a/src/Symbol/Symbol.lua b/src/Symbol/Symbol.lua index 9d28102..563c38a 100644 --- a/src/Symbol/Symbol.lua +++ b/src/Symbol/Symbol.lua @@ -1,3 +1,4 @@ +--!nonstrict --[[ Symbols have the type 'userdata', but when printed or coerced to a string, the symbol will turn into the string given as its name. diff --git a/src/Timers/__tests__/Timers.spec.lua b/src/Timers/__tests__/Timers.spec.lua index e0a32b5..7ad8e14 100644 --- a/src/Timers/__tests__/Timers.spec.lua +++ b/src/Timers/__tests__/Timers.spec.lua @@ -2,16 +2,17 @@ return function() local Timers = script.Parent.Parent local makeTimerImpl = require(Timers.makeTimerImpl) local LuauPolyfill = Timers.Parent - local createSpy = require(Timers.Parent.createSpy) local Packages = LuauPolyfill.Parent local JestGlobals = require(Packages.Dev.JestGlobals) local jestExpect = JestGlobals.expect + local jest = JestGlobals.jest local Timeout - local mockTime, timeouts + local mockTime: number + local timeouts - local function advanceTime(amount) + local function advanceTime(amount: number) -- Account for milliseconds to seconds conversion here, since Timeout -- will make the same adjustment mockTime += amount / 1000 @@ -20,7 +21,7 @@ return function() end end - local function mockDelay(delayTime, callback) + local function mockDelay(delayTime: number, callback) local targetTime = mockTime + delayTime timeouts[callback] = function(currentTime) if currentTime >= targetTime then @@ -38,59 +39,59 @@ return function() describe("Delay override logic", function() it("should not run delayed callbacks immediately", function() - local callbackSpy = createSpy() - Timeout.setTimeout(callbackSpy.value, 50) + local callbackSpy = jest.fn() + Timeout.setTimeout(callbackSpy, 50) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() end) it("should run callbacks after timers have been advanced sufficiently", function() - local callbackSpy = createSpy() - Timeout.setTimeout(callbackSpy.value, 100) + local callbackSpy = jest.fn() + Timeout.setTimeout(callbackSpy, 100) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() advanceTime(50) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() advanceTime(50) - jestExpect(callbackSpy.callCount).toEqual(1) + jestExpect(callbackSpy).toHaveBeenCalledTimes(1) end) end) describe("Timeout", function() it("should run exactly once", function() - local callbackSpy = createSpy() - Timeout.setTimeout(callbackSpy.value, 100) + local callbackSpy = jest.fn() + Timeout.setTimeout(callbackSpy, 100) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() advanceTime(100) - jestExpect(callbackSpy.callCount).toEqual(1) + jestExpect(callbackSpy).toHaveBeenCalledTimes(1) advanceTime(100) - jestExpect(callbackSpy.callCount).toEqual(1) + jestExpect(callbackSpy).toHaveBeenCalledTimes(1) advanceTime(1) - jestExpect(callbackSpy.callCount).toEqual(1) + jestExpect(callbackSpy).toHaveBeenCalledTimes(1) end) it("should be called with the given args", function() - local callbackSpy = createSpy() - Timeout.setTimeout(callbackSpy.value, 100, "hello", "world") + local callbackSpy = jest.fn() + Timeout.setTimeout(callbackSpy, 100, "hello", "world") advanceTime(100) - jestExpect(callbackSpy.callCount).toEqual(1) - callbackSpy:assertCalledWith("hello", "world") + jestExpect(callbackSpy).toHaveBeenCalledTimes(1) + jestExpect(callbackSpy).toHaveBeenCalledWith("hello", "world") end) it("should not run if cancelled before it is scheduled to run", function() - local callbackSpy = createSpy() - local task = Timeout.setTimeout(callbackSpy.value, 100) + local callbackSpy = jest.fn() + local task = Timeout.setTimeout(callbackSpy, 100) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() Timeout.clearTimeout(task) advanceTime(100) - jestExpect(callbackSpy.callCount).toEqual(0) + jestExpect(callbackSpy).never.toHaveBeenCalled() end) end) end diff --git a/src/Timers/init.lua b/src/Timers/init.lua index 0b3e8c4..a87fea6 100644 --- a/src/Timers/init.lua +++ b/src/Timers/init.lua @@ -1,3 +1,4 @@ +--!strict local makeTimerImpl = require(script.makeTimerImpl) - +export type Timeout = makeTimerImpl.Timeout return makeTimerImpl(delay) diff --git a/src/Timers/makeTimerImpl.lua b/src/Timers/makeTimerImpl.lua index a55e8a1..8f5c24b 100644 --- a/src/Timers/makeTimerImpl.lua +++ b/src/Timers/makeTimerImpl.lua @@ -1,14 +1,15 @@ +--!strict local Status = newproxy(false) type TaskStatus = number -type Task = { [Status]: TaskStatus } +export type Timeout = { [typeof(Status)]: TaskStatus } local SCHEDULED = 1 local DONE = 2 local CANCELLED = 3 return function(delayImpl) - local function setTimeout(callback, delayTime: number, ...): Task + local function setTimeout(callback, delayTime: number?, ...): Timeout local args = { ... } local task = { [Status] = SCHEDULED, @@ -20,7 +21,7 @@ return function(delayImpl) end -- To mimic the JS interface, we're expecting delayTime to be in ms - local delayTimeMs = delayTime / 1000 + local delayTimeMs = delayTime :: number / 1000 delayImpl(delayTimeMs, function() if task[Status] == SCHEDULED then callback(unpack(args)) @@ -31,7 +32,7 @@ return function(delayImpl) return task end - local function clearTimeout(task: Task) + local function clearTimeout(task: Timeout) if task[Status] == SCHEDULED then task[Status] = CANCELLED end diff --git a/src/WeakMap.lua b/src/WeakMap.lua index 0dd523e..f7cb81f 100644 --- a/src/WeakMap.lua +++ b/src/WeakMap.lua @@ -1,8 +1,9 @@ +--!strict export type WeakMap = { -- method definitions - get: (WeakMap, T) -> V, - set: (WeakMap, T, V) -> WeakMap, - has: (WeakMap, T) -> boolean, + get: (self: WeakMap, T) -> V, + set: (self: WeakMap, T, V) -> WeakMap, + has: (self: WeakMap, T) -> boolean, } local WeakMap = {} diff --git a/src/__tests__/Set.spec.lua b/src/__tests__/Set.spec.lua index 6f70fab..0fe98f1 100644 --- a/src/__tests__/Set.spec.lua +++ b/src/__tests__/Set.spec.lua @@ -1,3 +1,5 @@ +--!nonstrict +-- Luau FIXME: Type '{ @metatable Set, {| _array: {any}, _map: { [any]: boolean }, size: number |} }' could not be converted into '(Array | Iterable | Set | string)?'; none of the union options are compatible return function() local LuauPolyfill = script.Parent.Parent local Set = require(LuauPolyfill.Set) diff --git a/src/console/.robloxrc b/src/console/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/console/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/console/__tests__/console.spec.lua b/src/console/__tests__/console.spec.lua index 8794179..45b1ee4 100644 --- a/src/console/__tests__/console.spec.lua +++ b/src/console/__tests__/console.spec.lua @@ -1,4 +1,5 @@ return function() + type Function = (...any) -> ...any local consoleModule = script.Parent.Parent local makeConsoleImpl = require(consoleModule.makeConsoleImpl) @@ -14,7 +15,7 @@ return function() table.insert(capturedPrints, table.concat({ ... }, " ")) end - local function overridePrint(fn, ...) + local function overridePrint(fn: Function, ...) local originalPrint = getfenv(fn).print getfenv(fn).print = capturePrint fn(...) @@ -26,7 +27,7 @@ return function() table.insert(capturedWarns, table.concat({ ... }, " ")) end - local function overrideWarn(fn, ...) + local function overrideWarn(fn: Function, ...) local originalWarn = getfenv(fn).warn getfenv(fn).warn = captureWarn fn(...) diff --git a/src/console/init.lua b/src/console/init.lua index 82bc918..a456f86 100644 --- a/src/console/init.lua +++ b/src/console/init.lua @@ -1,3 +1,4 @@ +--!strict local makeConsoleImpl = require(script.makeConsoleImpl) return makeConsoleImpl() diff --git a/src/console/makeConsoleImpl.lua b/src/console/makeConsoleImpl.lua index 8b674e0..6a32286 100644 --- a/src/console/makeConsoleImpl.lua +++ b/src/console/makeConsoleImpl.lua @@ -1,3 +1,4 @@ +--!strict local INDENT = " " local inspect = require(script.Parent.Parent.util).inspect diff --git a/src/createSpy.lua b/src/createSpy.lua deleted file mode 100644 index b3e6b36..0000000 --- a/src/createSpy.lua +++ /dev/null @@ -1,65 +0,0 @@ ---[[ - A utility used to create a function spy that can be used to robustly test - that functions are invoked the correct number of times and with the correct - number of arguments. - - This should only be used in tests. -]] --- FIXME: Replace this with jest-roblox builtins - -local function createSpy(inner) - local self = { - callCount = 0, - values = {}, - valuesLength = 0, - } - - self.value = function(...) - self.callCount = self.callCount + 1 - self.values = { ... } - self.valuesLength = select("#", ...) - - if inner ~= nil then - return inner(...) - end - return nil - end - - self.assertCalledWith = function(_, ...) - local len = select("#", ...) - - if self.valuesLength ~= len then - error(("Expected %d arguments, but was called with %d arguments"):format(self.valuesLength, len), 2) - end - - for i = 1, len do - local expected = select(i, ...) - - assert(self.values[i] == expected, "value differs") - end - end - - self.captureValues = function(_, ...) - local len = select("#", ...) - local result = {} - - assert(self.valuesLength == len, "length of expected values differs from stored values") - - for i = 1, len do - local key = select(i, ...) - result[key] = self.values[i] - end - - return result - end - - setmetatable(self, { - __index = function(_, key) - error(("%q is not a valid member of spy"):format(key)) - end, - }) - - return self -end - -return createSpy diff --git a/src/extends.lua b/src/extends.lua index 042f1e5..1037463 100644 --- a/src/extends.lua +++ b/src/extends.lua @@ -1,3 +1,4 @@ +--!nonstrict --[[ deviation: Our constructors currently have no notion of 'super' so any such behavior in upstream JS must be implemented manually by setting fields diff --git a/src/init.lua b/src/init.lua index de657b8..400bf96 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1,3 +1,4 @@ +--!strict local Array = require(script.Array) local Error = require(script.Error) local mapModule = require(script.Map) @@ -16,6 +17,7 @@ export type PromiseLike = PromiseModule.PromiseLike export type Promise = PromiseModule.Promise export type Set = Set.Set +export type Timeout = Timers.Timeout export type WeakMap = WeakMap.WeakMap return { diff --git a/src/instanceof.lua b/src/instanceof.lua index c355ac6..4b601ea 100644 --- a/src/instanceof.lua +++ b/src/instanceof.lua @@ -1,3 +1,4 @@ +--!nonstrict -- polyfill for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof return function(tbl, class) assert(typeof(class) == "table", "Received a non-table as the second argument for instanceof") diff --git a/src/util/.robloxrc b/src/util/.robloxrc new file mode 100644 index 0000000..edadae6 --- /dev/null +++ b/src/util/.robloxrc @@ -0,0 +1,8 @@ +{ + "language": { + "mode": "strict" + }, + "lint": { + "*": "enabled" + } +} diff --git a/src/util/__tests__/inspect.spec.lua b/src/util/__tests__/inspect.spec.lua index 4a5729c..e1cbdcc 100644 --- a/src/util/__tests__/inspect.spec.lua +++ b/src/util/__tests__/inspect.spec.lua @@ -9,6 +9,8 @@ return function() local Promise = require(Packages.Dev.Promise) local inspect = require(srcWorkspace).util.inspect local Set = require(srcWorkspace).Set + type Array = { [number]: T } + type Object = { [string]: any } describe("inspect", function() -- it("undefined", function() @@ -63,7 +65,7 @@ return function() -- deviation: Lua does not handle nil elements jestExpect(inspect({ true })).toBe("[true]") jestExpect(inspect({ 1, 0 / 0 })).toBe("[1, NaN]") - jestExpect(inspect({ { "a", "b" }, "c" })).toBe('[["a", "b"], "c"]') + jestExpect(inspect({ { "a", "b" }, "c" } :: Array)).toBe('[["a", "b"], "c"]') jestExpect(inspect({ { {} } })).toBe("[[[]]]") jestExpect(inspect({ { { "a" } } })).toBe("[[[Array]]]") @@ -83,13 +85,13 @@ return function() -- jestExpect(inspect({})).toBe("{}") jestExpect(inspect({ a = 1 })).toBe("{ a: 1 }") jestExpect(inspect({ a = 1, b = 2 })).toBe("{ a: 1, b: 2 }") - jestExpect(inspect({ array = { false, 0 } })).toBe("{ array: [false, 0] }") + jestExpect(inspect({ array = { false, 0 } :: Array })).toBe("{ array: [false, 0] }") jestExpect(inspect({ a = { b = {} } })).toBe("{ a: { b: [] } }") jestExpect(inspect({ a = { b = { c = 1 } } })).toBe("{ a: { b: [Object] } }") - jestExpect(inspect({ [3.14159] = true, 1, 2 })).toBe("{ 1, 2, 3.14159: true }") - jestExpect(inspect({ 1, 2, [-3] = 3 })).toBe("{ 1, 2, -3: 3 }") + jestExpect(inspect({ [3.14159] = true :: any, 1, 2 } :: any)).toBe("{ 1, 2, 3.14159: true }") + jestExpect(inspect({ 1, 2, [-3] = 3 } :: any)).toBe("{ 1, 2, -3: 3 }") -- ROBLOX deviation: -- local map = Object.create(nil) @@ -99,8 +101,10 @@ return function() end) it("Set", function() - jestExpect(inspect(Set.new({ 31337, "foo" }))).toBe('Set (2) [31337, "foo"]') - jestExpect(inspect(Set.new({ Set.new({ 90210, "baz" }) }))).toBe('Set (1) [Set (2) [90210, "baz"]]') + jestExpect(inspect(Set.new({ 31337, "foo" } :: Array))).toBe('Set (2) [31337, "foo"]') + jestExpect(inspect(Set.new({ Set.new({ 90210, "baz" } :: Array) }))).toBe( + 'Set (1) [Set (2) [90210, "baz"]]' + ) jestExpect(inspect(Set.new({}))).toBe("Set []") end) @@ -136,10 +140,10 @@ return function() it("handles toJSON function that uses this", function() local object = { str = "Hello World!", + toJSON = function(self) + return self.str + end, } - function object.toJSON() - return object.str - end jestExpect(inspect(object)).toBe("Hello World!") end) diff --git a/src/util/inspect.lua b/src/util/inspect.lua index 5514cef..8726f08 100644 --- a/src/util/inspect.lua +++ b/src/util/inspect.lua @@ -41,7 +41,7 @@ local function getTableLength(tbl) return length - 1 end -local function sortKeysForPrinting(a, b) +local function sortKeysForPrinting(a: any, b) local typeofA = type(a) local typeofB = type(b)