-
Notifications
You must be signed in to change notification settings - Fork 31
Port codemod into lute #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bf078e3
transform subcommand
skberkeley 10b2e0f
transform example invoked via `lute transform @self/transformer ./exa…
skberkeley 789970f
use lute.load to load migration
skberkeley 339ef97
unit test for lute transform
skberkeley cdbd054
add linux lute path option
skberkeley 71d72c8
stylua
skberkeley aaf915b
Merge branch 'primary' into transform_subcommand
skberkeley 53743fe
move string lib
skberkeley 873f0d9
remove path library in favor of std one
skberkeley d973812
other small comments
skberkeley 7b9e493
Merge branch 'primary' into transform_subcommand
skberkeley b70036f
Merge branch 'primary' into transform_subcommand
skberkeley e3400d7
Merge branch 'primary' into transform_subcommand
skberkeley eb865c2
Merge branch 'primary' into transform_subcommand
skberkeley 0f7afd8
delete pcall comment
skberkeley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| local x = math.sqrt(-1) | ||
| -- x ~= x should become math.isnan(x) | ||
| local b = x ~= x |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| local parser = require("@std/syntax/parser") | ||
| local printer = require("@std/syntax/printer") | ||
| local visitor = require("@std/syntax/visitor") | ||
| local luau = require("@lute/luau") | ||
|
|
||
| function transformation(ctx) | ||
| local parsedResult = parser.parsefile(ctx.source) | ||
|
|
||
| local v = visitor.createVisitor() | ||
|
|
||
| v.visitBinary = function(node: luau.AstExprBinary) | ||
| if node.operator.text ~= "~=" then | ||
| return true | ||
| end | ||
|
|
||
| if node.lhsoperand.tag ~= "local" or node.rhsoperand.tag ~= "local" then | ||
| return true | ||
| end | ||
|
|
||
| if node.lhsoperand.token.text ~= node.rhsoperand.token.text then | ||
| return true | ||
| end | ||
|
|
||
| local operand: luau.AstExprLocal = node.lhsoperand | ||
| operand.token.leadingtrivia = {} | ||
| operand.token.trailingtrivia = {} | ||
| local openingPosition: luau.Position = operand.token.position; | ||
|
|
||
| -- transform node into an AstExprCall | ||
| (node :: any).operator = nil | ||
| (node :: any).lhsoperand = nil | ||
| (node :: any).rhsoperand = nil | ||
|
|
||
| ((node :: any) :: luau.AstExprCall).tag = "call" | ||
|
|
||
| local func: luau.AstExpr = { | ||
| tag = "global", | ||
| name = { | ||
| leadingtrivia = {}, | ||
| position = openingPosition, | ||
| text = "math.isnan", | ||
| trailingtrivia = {}, | ||
| }, | ||
| } | ||
| ((node :: any) :: luau.AstExprCall).func = func | ||
|
|
||
| local openparens: luau.Token<"("> = { | ||
| leadingtrivia = {}, | ||
| position = { line = openingPosition.line, column = openingPosition.column + #"math.isnan" }, | ||
| text = "(", | ||
| trailingtrivia = {}, | ||
| } | ||
| ((node :: any) :: luau.AstExprCall).openparens = openparens | ||
|
|
||
| local arguments: luau.Punctuated<luau.AstExpr> = { { node = operand } } | ||
| ((node :: any) :: luau.AstExprCall).arguments = arguments | ||
|
|
||
| local closeparens: luau.Token<")"> = { | ||
| leadingtrivia = {}, | ||
| position = { | ||
| line = openingPosition.line, | ||
| column = openingPosition.column + #"math.isnan(" + #operand.token.text, | ||
| }, | ||
| text = ")", | ||
| trailingtrivia = {}, | ||
| } | ||
| ((node :: any) :: luau.AstExprCall).closeparens = closeparens; | ||
|
|
||
| ((node :: any) :: luau.AstExprCall).self = false | ||
|
|
||
| local argLocation: luau.Location = { | ||
| begin = { line = openingPosition.line, column = openingPosition.column + #"math.isnan(" }, | ||
| ["end"] = { | ||
| line = openingPosition.line, | ||
| column = openingPosition.column + #"math.isnan(" + #operand.token.text, | ||
| }, | ||
| } | ||
| ((node :: any) :: luau.AstExprCall).argLocation = argLocation | ||
|
|
||
| return false | ||
| end | ||
|
|
||
| visitor.visitBlock(parsedResult.root, v) | ||
|
|
||
| return printer.printfile(parsedResult) | ||
| end | ||
|
|
||
| return transformation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| local fs = require("@lute/fs") | ||
| local luau = require("@lute/luau") | ||
| local pathLib = require("@std/path") | ||
|
|
||
| local arguments = require("@self/lib/arguments") | ||
| local files = require("@self/lib/files") | ||
| local types = require("@self/lib/types") | ||
|
|
||
| local function exhaustiveMatch(value: never): never | ||
| error(`Unknown value in exhaustive match: {value}`) | ||
| end | ||
|
|
||
| local function loadFromPath(path: string): ...any | ||
| local migrationHandle = fs.open(path, "r") | ||
| assert(migrationHandle ~= nil, `Failed to open migration file at '{path}'`) | ||
|
|
||
| local migrationBytecode = luau.compile(fs.read(migrationHandle)) | ||
|
|
||
| fs.close(migrationHandle) | ||
|
|
||
| return luau.load(migrationBytecode, path)() | ||
| end | ||
|
|
||
| local function loadMigration(path: string): types.Migration | ||
| local success, loaded = pcall(loadFromPath, path) | ||
| assert(success, `{path} failed to require: {loaded}`) | ||
| assert(loaded, `{path} is missing a return`) | ||
|
|
||
| if typeof(loaded) == "function" then | ||
| local migration = {} | ||
| migration.transform = loaded | ||
| return migration :: types.Migration -- FIXME: Luau | ||
| elseif typeof(loaded) == "table" then | ||
| -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1803 for the type assertions | ||
| assert( | ||
| typeof((loaded :: any).transform) == "function", | ||
| `{path} must contain a 'transform' property of type function` | ||
| ) | ||
| if (loaded :: any).options then | ||
| assert(typeof(loaded.options) == "table", `Expected return of {path}.options to be a table`) | ||
| for _, option in loaded.options do | ||
| assert(typeof(option.name) == "string", `Expected option.name to be a string`) | ||
| assert( | ||
| option.kind == "string" or option.kind == "boolean", | ||
| `Expected option.kind to be either 'string' or 'boolean', got {option.kind}` | ||
| ) | ||
| end | ||
| end | ||
| if (loaded :: any).initialize then | ||
| assert(typeof(loaded.initialize) == "function", `Expected {path}.initialize to be a function`) | ||
| end | ||
| return loaded :: types.Migration | ||
| else | ||
| error(`Expected '{path}' to return a function or table, got {typeof(loaded)}`) | ||
| end | ||
| end | ||
|
|
||
| local function processMigrationOptions(migration: types.Migration, options: { [string]: string }): { [string]: any } | ||
| if migration.options == nil then | ||
| return table.freeze({}) :: any -- FIXME(Luau): https://github.com/luau-lang/luau/issues/1802 | ||
| end | ||
|
|
||
| local processedOptions: { [string]: string | boolean } = {} | ||
|
|
||
| for _, option in migration.options do | ||
| local value = options[option.name] | ||
| if value then | ||
| if option.kind == "boolean" then | ||
| local lowered = value:lower() | ||
| assert( | ||
| lowered == "true" or lowered == "false", | ||
| `Option '--{option.name}' expects a boolean value 'true' or 'false'` | ||
| ) | ||
| processedOptions[option.name] = lowered == "true" | ||
| elseif option.kind == "string" then | ||
| processedOptions[option.name] = value | ||
| else | ||
| exhaustiveMatch(option.kind) | ||
| end | ||
| else | ||
| assert(option.default ~= nil, `Missing required option '--{option.name}'`) | ||
| processedOptions[option.name] = option.default | ||
| end | ||
| end | ||
|
|
||
| return table.freeze(processedOptions) | ||
| end | ||
|
|
||
| local function applyMigration( | ||
| migration: types.Migration, | ||
| paths: { pathLib.path }, | ||
| options: { [string]: string | boolean }, | ||
| dryRun: boolean | ||
| ) | ||
| local deletedPaths = {} | ||
|
|
||
| if migration.initialize then | ||
| print("Initializing migration") | ||
| local ctx = { | ||
| options = options, | ||
| } | ||
| migration.initialize(ctx) | ||
| end | ||
|
|
||
| for _, pathObj in paths do | ||
| local pathStr = pathLib.format(pathObj) | ||
|
|
||
| print(`Applying to {pathStr}`) | ||
|
|
||
| local source = fs.readfiletostring(pathStr) | ||
|
|
||
| local ctx = { | ||
| path = pathStr, | ||
| source = source, | ||
| options = options, | ||
| } | ||
|
|
||
| -- TODO: should we wrap in pcall? For now we don't do this to preserve stack trace | ||
| local result = migration.transform(ctx) | ||
|
|
||
| assert(typeof(result) == "string", `Transformed expected to return string, but got {typeof(result)}`) | ||
|
|
||
| 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 | ||
| end | ||
|
|
||
| if not dryRun then | ||
| for _, pathStr in deletedPaths do | ||
| print(`Deleting {pathStr}`) | ||
| fs.remove(pathStr) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local function main(...: string) | ||
| local args = arguments.parse({ ... }) | ||
|
|
||
| if args.dryRun then | ||
| print("Executing in dry run mode") | ||
| end | ||
|
|
||
| print(`Loading migration '{args.migrationPath}'`) | ||
| local migration = loadMigration(args.migrationPath) | ||
|
|
||
| print("Finding source files") -- todo: might type error | ||
| local files = files.getSourceFiles(args.filePaths) | ||
| if #files == 0 then | ||
| error("error: no source files provided") | ||
| end | ||
|
|
||
| print("Processing migration options") | ||
| local migrationOptions = processMigrationOptions(migration, args.migrationOptions) | ||
|
|
||
| print("Applying migration") | ||
| applyMigration(migration, files, migrationOptions, args.dryRun) | ||
|
|
||
| print(`Processed {#files} files!`) | ||
| end | ||
|
|
||
| main(...) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| local string = require("@std/string") | ||
|
|
||
| type Config = { | ||
| --- Prevent making changes to files | ||
| dryRun: boolean, | ||
| --- Path to the migration module to execute | ||
| migrationPath: string, | ||
| --- Configuration flags to pass to the migration | ||
| migrationOptions: { [string]: string }, | ||
| --- List of file paths to process | ||
| --- Note: no directory traversal or filtering has been applied on this list | ||
| filePaths: { string }, | ||
| } | ||
|
|
||
| local function parse(arguments: { string }): Config | ||
| -- TODO: replace with proper CLI system | ||
| -- For now, first argument is the transformer script, whilst the rest are either flags or files to apply it on | ||
|
|
||
| if #arguments < 2 then | ||
| error("Usage: lute transform <transformer script> [options...] <files...>") | ||
| end | ||
|
|
||
| local config = { | ||
| dryRun = false, | ||
| migrationPath = nil :: string?, | ||
| migrationOptions = {}, | ||
| filePaths = {}, | ||
| } | ||
|
|
||
| local i = 1 | ||
| while i <= #arguments do | ||
| local argument = arguments[i] | ||
|
|
||
| if string.startswith(argument, "--") then | ||
| local name = string.removeprefix(argument, "--") | ||
|
|
||
| if config.migrationPath == nil then | ||
| -- Options before the codemod file are parsed as options for the tool itself | ||
| if name == "dry-run" then | ||
| config.dryRun = true | ||
| else | ||
| error(`Unknown flag '--{name}'`) | ||
| end | ||
| else | ||
| i += 1 | ||
| assert(i <= #arguments, `Missing value for '{name}'`) | ||
| local value = arguments[i] | ||
|
|
||
| config.migrationOptions[name] = value | ||
| end | ||
| else | ||
| if config.migrationPath == nil then | ||
| config.migrationPath = argument | ||
| else | ||
| table.insert(config.filePaths, argument) | ||
| end | ||
| end | ||
|
|
||
| i += 1 | ||
| end | ||
|
|
||
| assert(config.migrationPath, "ASSERTION FAILED: codemodPath ~= nil") | ||
|
|
||
| return config | ||
| end | ||
|
|
||
| return { | ||
| parse = parse, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| local fs = require("@lute/fs") | ||
| local process = require("@lute/process") | ||
|
|
||
| local ignore = require("./ignore") | ||
| local path = require("@std/path") | ||
| local string = require("@std/string") | ||
|
|
||
| local function extend<T>(tbl: { T }, other: { T }) | ||
| for _, entry in other do | ||
| table.insert(tbl, entry) | ||
| end | ||
| end | ||
|
|
||
| local function traverseDirectoryRecursive(directory: path.path, ignoreResolver: ignore.IgnoreResolver): { path.path } | ||
| local results = {} | ||
|
|
||
| for _, entry in fs.listdir(path.format(directory)) do | ||
| local fullPath = path.join(directory, entry.name) | ||
|
|
||
| if entry.type == "dir" then | ||
| if ignoreResolver:isIgnoredDirectory(fullPath) then | ||
| print("Skipping", fullPath) | ||
| continue | ||
| end | ||
|
|
||
| extend(results, traverseDirectoryRecursive(fullPath, ignoreResolver)) | ||
| else | ||
| -- TODO: allow customisation of the filter when traversing a directory. e.g., globs? file endings? ignore paths? | ||
| if string.endswith(entry.name, ".luau") or string.endswith(entry.name, ".lua") then | ||
| if ignoreResolver:isIgnoredFile(fullPath) then | ||
| print("Skipping", fullPath) | ||
| continue | ||
| end | ||
| table.insert(results, fullPath) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| return results | ||
| end | ||
|
|
||
| local function getSourceFiles(paths: { string }): { path.path } | ||
| local files = {} | ||
|
|
||
| local ignoreResolver = ignore.new() | ||
|
|
||
| for _, filepath in paths do | ||
| local pathObj = path.parse(filepath) | ||
|
|
||
| if not path.isabsolute(pathObj) then | ||
| pathObj = path.join(process.cwd(), pathObj) | ||
| end | ||
|
|
||
| filepath = path.format(pathObj) | ||
| print(filepath) | ||
| if fs.type(filepath) == "dir" then | ||
| extend(files, traverseDirectoryRecursive(pathObj, ignoreResolver)) | ||
| else | ||
| table.insert(files, pathObj) | ||
| end | ||
| end | ||
|
|
||
| return files | ||
| end | ||
|
|
||
| return { | ||
| getSourceFiles = getSourceFiles, | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.