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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions lute/cli/commands/transform/init.luau
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
local fs = require("@lute/fs")
local fs = require("@std/fs")
local luau = require("@std/luau")
local pathLib = require("@std/path")
local printer = require("@std/syntax/printer")
local syntax = require("@std/syntax")
local syntaxTypes = require("@std/syntax/types")

local arguments = require("@self/lib/arguments")
local files = require("@self/lib/files")
Expand Down Expand Up @@ -82,7 +81,8 @@ local function applyMigration(
migration: types.Migration,
paths: { pathLib.path },
options: { [string]: string | boolean },
dryRun: boolean
dryRun: boolean,
outputPath: string?
)
local deletedPaths = {}

Expand All @@ -94,6 +94,8 @@ local function applyMigration(
migration.initialize(ctx)
end

local outputFilePath = if outputPath then pathLib.parse(outputPath) else nil

for _, pathObj in paths do
local pathStr = pathLib.format(pathObj)

Expand All @@ -113,23 +115,21 @@ local function applyMigration(
-- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace
local result = migration.transform(ctx)

if typeof(result) == "string" then
if result == types.DELETION_MARKER then
print(`Marking {pathStr} for deletion`)
table.insert(deletedPaths, pathStr)
elseif result ~= source and not dryRun then
fs.writestringtofile(pathStr, result)
end
else
local replacements = result :: syntaxTypes.replacements

local serialized = printer.printfile(parseresult, replacements)
local toWrite = if typeof(result) == "string" then result else printer.printfile(parseresult, result)

if serialized == types.DELETION_MARKER then
if toWrite == types.DELETION_MARKER then
if outputFilePath then
print(`Skipping writing {pathStr} as it was marked for deletion`)
else
print(`Marking {pathStr} for deletion`)
table.insert(deletedPaths, pathStr)
elseif serialized ~= source and not dryRun then
fs.writestringtofile(pathStr, serialized)
end
elseif not dryRun then
if outputFilePath then
assert(outputFilePath ~= nil)
fs.writestringtofile(outputFilePath, toWrite)
elseif toWrite ~= source then
fs.writestringtofile(pathStr, toWrite)
end
end
end
Expand All @@ -149,6 +149,10 @@ local function main(...: string)
print("Executing in dry run mode")
end

if args.outputFile then
print(`Output path specified: '{args.outputFile}'`)
end

print(`Loading migration '{args.migrationPath}'`)
local migration = loadMigration(args.migrationPath)

Expand All @@ -162,7 +166,7 @@ local function main(...: string)
local migrationOptions = processMigrationOptions(migration, args.migrationOptions)

print("Applying migration")
applyMigration(migration, files, migrationOptions, args.dryRun)
applyMigration(migration, files, migrationOptions, args.dryRun, args.outputFile)

print(`Processed {#files} files!`)
end
Expand Down
11 changes: 11 additions & 0 deletions lute/cli/commands/transform/lib/arguments.luau
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type Config = {
--- List of file paths to process
--- Note: no directory traversal or filtering has been applied on this list
filePaths: { string },
--- Path to output file (only works if a single input file is specified)
outputFile: string?,
}

local function parse(arguments: { string }): Config
Expand All @@ -25,6 +27,7 @@ local function parse(arguments: { string }): Config
migrationPath = nil :: string?,
migrationOptions = {},
filePaths = {},
outputFile = nil,
}

local i = 1
Expand All @@ -38,6 +41,10 @@ local function parse(arguments: { string }): Config
-- Options before the codemod file are parsed as options for the tool itself
if name == "dry-run" then
config.dryRun = true
elseif name == "output" then
i += 1
assert(i <= #arguments, `Missing value for '--output'`)
config.outputFile = arguments[i]
else
error(`Unknown flag '--{name}'`)
end
Expand All @@ -60,6 +67,10 @@ local function parse(arguments: { string }): Config
end

assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil")
assert(
if config.outputFile ~= nil then #config.filePaths == 1 else true,
"ASSERTION FAILED: When specifying an output file, only one input file is allowed"
)

return config
end
Expand Down
3 changes: 2 additions & 1 deletion lute/cli/commands/transform/lib/types.luau
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local syntax = require("@std/syntax")
local syntaxTypes = require("@std/syntax/types")

export type Context<Options = { [any]: any }> = {
path: string,
Expand All @@ -7,7 +8,7 @@ export type Context<Options = { [any]: any }> = {
options: Options,
}

export type Transformer = (Context) -> string
export type Transformer = (Context) -> string | syntaxTypes.replacements

export type ConfigOption =
{
Expand Down
73 changes: 73 additions & 0 deletions tests/cli/transform.test.luau
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local b = x ~= x
local lutePath = process.execpath()
local tmpDir = system.tmpdir()
local transformeePath = path.format(path.join(tmpDir, "transformee.luau"))
local outputPath = path.format(path.join(tmpDir, "transformee_output.luau"))

test.suite("lute transform", function(suite)
suite:beforeeach(function()
Expand All @@ -24,6 +25,9 @@ test.suite("lute transform", function(suite)
suite:aftereach(function()
-- Remove the transformee file
fs.remove(transformeePath)
if fs.exists(outputPath) then
fs.remove(outputPath)
end
end)

suite:case("transform visitor style", function(assert)
Expand Down Expand Up @@ -73,6 +77,75 @@ test.suite("lute transform", function(suite)
-- Teardown
fs.remove(transformerDest)
end)

suite:case("transform output directory happy path", function(assert)
-- Setup
-- Copy examples/transformer.luau to build/transform_tests/transformer.luau
local transformerExample = path.format(path.join("examples", "query_transformer.luau"))
local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau"))
fs.copy(transformerExample, transformerDest)

-- Create output file
local h = fs.open(outputPath, "w+")
fs.close(h)

-- Do
-- Run the transformer on the transformee
local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath })
-- Check
assert.eq(result.exitcode, 0)

-- Check that the original file is unchanged
local transformeeHandle = fs.open(transformeePath, "r")
local transformeeContent = fs.read(transformeeHandle)
assert.eq(transformeeContent, TRANSFORMEE_CONTENT)
fs.close(transformeeHandle)

-- Check that the output file has been written to
h = fs.open(outputPath, "r")
local outputContent = fs.read(h)
assert.eq(outputContent:find("x ~= x"), nil)
assert.neq(outputContent:find("math.isnan(x)", 1, true), nil)
fs.close(h)

-- Teardown
fs.remove(transformerDest)
fs.remove(outputPath)
end)

suite:case("transform output creates file if it doesn't exist", function(assert)
-- Setup
-- Copy examples/transformer.luau to build/transform_tests/transformer.luau
local transformerExample = path.format(path.join("examples", "query_transformer.luau"))
local transformerDest = path.format(path.join(tmpDir, "query_transformer.luau"))
fs.copy(transformerExample, transformerDest)

-- Create output directory

-- Do
-- Run the transformer on the transformee
local result = process.run({ lutePath, "transform", "--output", outputPath, transformerDest, transformeePath })
-- Check
assert.eq(result.exitcode, 0)

-- Check that the original file is unchanged
local transformeeHandle = fs.open(transformeePath, "r")
local transformeeContent = fs.read(transformeeHandle)
assert.eq(transformeeContent, TRANSFORMEE_CONTENT)
fs.close(transformeeHandle)

-- Check that the output file has been created and written to
assert.eq(fs.exists(outputPath), true)
local h = fs.open(outputPath, "r")
local outputContent = fs.read(h)
assert.eq(outputContent:find("x ~= x"), nil)
assert.neq(outputContent:find("math.isnan(x)", 1, true), nil)
fs.close(h)

-- Teardown
fs.remove(transformerDest)
fs.remove(outputPath)
end)
end)

test.run()