Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
4351412
add merge and foreach to tableext
wmccrthy Nov 14, 2025
0d47b03
nit
wmccrthy Nov 14, 2025
e9c9f01
clean up
wmccrthy Nov 14, 2025
6fa7a7b
add support for replacing tokens
wmccrthy Nov 14, 2025
ea6f9e5
e2e cases boiler plate
wmccrthy Nov 14, 2025
009e6a1
query wrapper; overrides selectFromRoot so replacements are passed ar…
wmccrthy Nov 14, 2025
39272ed
stylua
wmccrthy Nov 14, 2025
33c3bd3
add apiRenameMigration case + repeated transform utils
wmccrthy Nov 15, 2025
42f1814
func rename
wmccrthy Nov 15, 2025
51d953e
use table.unpack
wmccrthy Nov 17, 2025
3cbd48a
test case name change
wmccrthy Nov 17, 2025
cee4ee4
boilerplate for roact->react test case
wmccrthy Nov 17, 2025
72959d5
Merge branch 'primary' into e2e-query-tests
wmccrthy Nov 17, 2025
1cdcea9
update to work w/ primary branch changes to parser/printer/visitor
wmccrthy Nov 17, 2025
ab0ae3b
fully working roact->react transform + test case(s)
wmccrthy Nov 17, 2025
2386e6f
Merge branch 'primary' into e2e-query-tests
wmccrthy Nov 17, 2025
2846da7
Discard changes to lute/std/libs/syntax/printer.luau
wmccrthy Nov 17, 2025
2fd76ca
add token printing back
wmccrthy Nov 17, 2025
fa1d1fb
remove comments + unused; nit cleanups
wmccrthy Nov 17, 2025
dcd144a
Merge branch 'primary' into e2e-query-tests
wmccrthy Nov 20, 2025
a5223cc
update to account for breaking changes
wmccrthy Nov 20, 2025
3b17c80
slightly closer to snapshot tests
wmccrthy Nov 20, 2025
ca49ddc
slightly more organized transform output
wmccrthy Nov 20, 2025
5231fba
Merge branch 'primary' into e2e-query-tests
wmccrthy Nov 20, 2025
9999b21
restructure tests s.t cases are stored in snapshot files rather than …
wmccrthy Nov 20, 2025
34aded2
restructure test to support file-based snapshots; gitignore transform…
wmccrthy Nov 20, 2025
82b71f2
Discard changes to lute/std/libs/syntax/printer.luau
wmccrthy Nov 20, 2025
ac5da79
Delete tests/std/syntax/e2eCases/minimal_repro.luau
wmccrthy Nov 20, 2025
96da218
remove api transform and use functional query style
wmccrthy Nov 21, 2025
1eef702
fix some disgusting type errs
wmccrthy Nov 21, 2025
041ebed
fix more type errs; remove comment
wmccrthy Nov 21, 2025
18f73e7
add exists helper on query and update tests accordingly
wmccrthy Nov 21, 2025
5b072d2
add tests for exists helper
wmccrthy Nov 21, 2025
f5499b1
use luau files for snapshots
wmccrthy Nov 21, 2025
8076ed2
remove accidental print
wmccrthy Nov 21, 2025
d6d76b1
remove redundancies
wmccrthy Nov 21, 2025
8dda54a
reorganize cases output according to feedbck
wmccrthy Nov 22, 2025
3ed9211
add missing type
wmccrthy Nov 22, 2025
56bc199
output git diff on test failure
wmccrthy Nov 23, 2025
49a7e71
Merge branch 'primary' into e2e-query-tests
wmccrthy Nov 24, 2025
3a7b52f
clean up nits from review
wmccrthy Nov 25, 2025
7b72cce
Merge branch 'primary' into e2e-query-tests
wmccrthy Dec 1, 2025
24fca16
Merge branch 'primary' into e2e-query-tests
wmccrthy Dec 8, 2025
3746aa6
Merge branch 'e2e-query-tests' of https://github.com/wmccrthy/lute in…
wmccrthy Dec 8, 2025
53c0989
use difftext in test
wmccrthy Dec 8, 2025
c16a9c1
use transform's output arg
wmccrthy Dec 9, 2025
bfbf400
apply first round of feedback
wmccrthy Dec 10, 2025
9229b97
reformat to use fs.walk
wmccrthy Dec 10, 2025
87c10c9
Merge branch 'primary' into e2e-query-tests
wmccrthy Dec 10, 2025
f12ba96
return type annotation instead of any annotation
wmccrthy Dec 10, 2025
19f1a90
Merge branch 'primary' into e2e-query-tests
wmccrthy Dec 12, 2025
523c56f
use line numbers in diff output
wmccrthy Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lute/std/libs/syntax/printer.luau
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,12 @@ local function printVisitor(replacements: types.replacements?)
return false
end
printer.visitToken = function(node: types.Token)
printer:printToken(node)
return false
if replacements ~= nil and replacements[node] then
return maybeReplace(node)
else
printer:printToken(node)
return false
end
end
printer.visitString = function(node: types.AstExprConstantString)
printer:printString(node)
Expand Down
59 changes: 59 additions & 0 deletions lute/std/libs/syntax/transform.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
local luau = require("@lute/luau")
local query = require("@std/syntax/query")
local printer = require("@std/syntax/printer")
local types = require("@std/syntax/types")
local tableext = require("@std/tableext")
export type query<T> = query.query<T>
type node = types.AstNode
type ParseResult = types.ParseResult

local transform = {}
transform.__index = transform

export type TransformData = {
replacements: types.replacements,
__fileRoot: types.AstStatBlock,
}

export type transform = typeof(setmetatable({} :: TransformData, transform))

-- transform is entry point for a codemod; exposes base tools you'll need

function transform.new(fileRoot: types.AstStatBlock)
local self = {
replacements = {} :: types.replacements,
__fileRoot = fileRoot,
}

return setmetatable(self, transform)
end

-- how do we enforce that this is always called on desc of our root?
function transform.selectFromRoot<T>(self: transform, ast: node, fn: (node) -> T?): query<T>
local q = query.selectFromRoot(ast, fn)
local t = self
q.replace = function<V>(self: query<V>, repl: (V) -> types.replacement?)
local replacements = query.replace(self, repl)
tableext.merge(t.replacements, replacements)
return replacements
end
return q
end

transform.query = transform.selectFromRoot

function transform.transform(self: transform): string
return printer.printnode(self.__fileRoot :: any, self.replacements)
end

return table.freeze(transform)

--[[

example usage:

local t = transform.new(block)

t:selectFromRoot()

]]
4 changes: 2 additions & 2 deletions lute/std/libs/syntax/types.luau
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export type AstNode = luau.AstNode

export type ParseResult = luau.ParseResult

export type replacement = string | AstNode
export type replacements = { [AstNode]: replacement }
export type replacement = string | AstNode | Token
export type replacements = { [AstNode | Token]: replacement }

return {}
2 changes: 2 additions & 0 deletions lute/std/libs/syntax/visitor.luau
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,8 @@ local function visit(node: types.AstNode, visitor: Visitor)
visitLocal(node, visitor)
elseif node.kind == "attribute" then
visitAttribute(node, visitor)
elseif node.istoken == true then
visitToken(node, visitor)
else
exhaustiveMatch(node.kind)
end
Expand Down
27 changes: 24 additions & 3 deletions lute/std/libs/tableext.luau
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,30 @@ function tableext.fold<K, V, A>(table: { [K]: V }, f: (A, V) -> A, initial: A):
return acc
end

function tableext.extend<T>(tbl: array<T>, other: array<T>)
for _, entry in other do
table.insert(tbl, entry)
function tableext.extend<T>(tbl: array<T>, ...: array<T>)
local extendees = table.pack(...)
extendees.n = nil
for _, extendee in extendees do
for _, entry in extendee do
table.insert(tbl, entry)
end
end
end

function tableext.merge<K, V>(tbl: { [K]: V }, ...: { [K]: V }): { [K]: V }
local mergees = table.pack(...)
mergees.n = nil
for _, mergee in mergees do
for key, val in mergee do
tbl[key] = val
end
end
return tbl
end

function tableext.foreach<K, V>(tbl: { [K]: V }, callback: (K, V) -> ())
for k, v in tbl do
callback(k, v)
end
end

Expand Down
61 changes: 61 additions & 0 deletions tests/std/syntax/e2eCases/apiSurfaceChange/cases.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
local types = require("../types")
type Case = types.Case

return {
-- Test 1: Simple function rename
{
initial = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.numFunc(1)
]],
expected = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.processNumber(1)
]],
},
-- Test 2: Function rename with type change (number -> string)
{
initial = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.arrayFunc({1, 2, 3})
]],
expected = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.processArray(table.unpack({1, 2, 3}))
]],
},
-- Test 3: Multiple function calls with renames and type changes
{
initial = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local a = sameLibrary.numFunc(42)
local b = sameLibrary.arrayFunc({10, 20, 30})
local c = sameLibrary.stringFunc("hello")
]],
expected = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local a = sameLibrary.processNumber(42)
local b = sameLibrary.processArray(table.unpack({10, 20, 30}))
local c = sameLibrary.handleText("hello")
]],
},
-- Test 4: Nested function calls
{
initial = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.numFunc(sameLibrary.getCount())
]],
expected = [[
local sameLibrary = require(packagesRoot.sameLibrary)

local result = sameLibrary.processNumber(sameLibrary.retrieveCount())
]],
},
} :: { Case }
44 changes: 44 additions & 0 deletions tests/std/syntax/e2eCases/apiSurfaceChange/init.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local luau = require("@lute/luau")
local t = require("@std/syntax/transform")
local utils = require("@std/syntax/utils")
local printer = require("@std/syntax/printer")
local qUtils = require("./utils")

local apiMap = {
numFunc = "processNumber",
stringFunc = "handleText",
arrayFunc = "processArray",
processData = "handleData",
getCount = "retrieveCount",
}

local argReplacements = {
arrayFunc = function(t: t.transform, node: luau.AstExprCall)
local arg = node.arguments[1].node
t.replacements[arg :: any] = `table.unpack({printer.printnode(arg :: any)})`
end,
}

local function transformCallArgs(t: t.transform, block: luau.AstStatBlock)
t:selectFromRoot(block, utils.isExprCall)
:filter(function(node)
return node.func.index and argReplacements[node.func.index.text] ~= nil
end)
:replace(function(node)
return node.func.index and argReplacements[node.func.index.text](t, node)
end)
end

local function transform(block: luau.AstStatBlock): string
local transform = t.new(block :: any)
qUtils.transformLibraryAccess(transform, block, "sameLibrary", function(expr, index: luau.Token)
return `{printer.printnode(expr :: any)}.{apiMap[index.text]}`
end)
transformCallArgs(transform, block)
return transform:transform()
end

return {
transform = transform,
cases = require("./apiSurfaceChange/cases"),
}
5 changes: 5 additions & 0 deletions tests/std/syntax/e2eCases/init.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
return {
mockLibrary = require("./e2eCases/libraryMigration"),
mockApiSurfaceChange = require("./e2eCases/apiSurfaceChange"),
mockRoactToReact = require("./e2eCases/roactToReact"),
}
55 changes: 55 additions & 0 deletions tests/std/syntax/e2eCases/libraryMigration/cases.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local types = require("../types")
type Case = types.Case

return {
{
initial = [[
local oldLibrary = require(packagesRoot.old)

local function myDependentFunction(input: { string }): boolean
for _, s in input do
if oldLibrary.oldFunction(input) then
return true
end
end
return false
end

local function myIndexUsingDependentFunction(input: string): boolean
return oldLibrary.anotherFunction(input)
end
]],
expected = [[
local newLibrary = require(packagesRoot.new)

local function myDependentFunction(input: { string }): boolean
for _, s in input do
if newLibrary.newFunction(input) then
return true
end
end
return false
end

local function myIndexUsingDependentFunction(input: string): boolean
return newLibrary.anotherNewFunction(input)
end
]],
},
{
initial = [[
local oldLibraryDiffAlias = require(root.nested.packages.old)

local myEl = oldLibraryDiffAlias.oldFunction("lol")

return oldLibraryDiffAlias.anotherFunction(myEl)
]],
expected = [[
local newLibrary = require(root.nested.packages.new)

local myEl = newLibrary.newFunction("lol")

return newLibrary.anotherNewFunction(myEl)
]],
},
} :: { Case }
56 changes: 56 additions & 0 deletions tests/std/syntax/e2eCases/libraryMigration/init.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
local luau = require("@lute/luau")
local t = require("@std/syntax/transform")
local utils = require("@std/syntax/utils")
local printer = require("@std/syntax/printer")
local cases = require("./libraryMigration/cases")
local qUtils = require("./utils")
local getRequireStatements = qUtils.getRequireStatements
local transformLibraryAccess = qUtils.transformLibraryAccess

-- local newFunction = newLibrary.newFunction
-- local oldFunction = oldLibrary.oldFunction
local function transformLocal() -- need to transform newFunction use
end

-- maps oldLibrary function names to newLibrary functions
local functionMap = {
["oldFunction"] = "newFunction",
["anotherFunction"] = "anotherNewFunction",
}

local function transformRequires(q: t.transform, block: luau.AstStatBlock): string
local oldLibRequires = getRequireStatements(block, "old")

q:selectFromRoot(oldLibRequires.nodes[1] :: any, utils.isExprCall)
:map(function(node)
return node.arguments[1].node.index
end)
:replace(function(node)
return "new"
end)

local oldLibVar = oldLibRequires.nodes[1].variables[1].node
local oldLibAlias
q:selectFromRoot(oldLibVar :: any, utils.isToken):replace(function(node: luau.Token)
oldLibAlias = node.text
return "newLibrary"
end)
return oldLibAlias
end

local function transform(block: luau.AstStatBlock): string
local transform = t.new(block :: any)
local oldLibAlias = transformRequires(transform, block)
transformLibraryAccess(transform, block, oldLibAlias, function(_, index)
local oldFunction = index.text
return `newLibrary.{functionMap[oldFunction]}`
end)
return transform:transform()
end

return {
transform = transform,
cases = cases,
}

-- having parent nodes will be goated! context in the tree is huge
Loading