Skip to content

Commit

Permalink
feat(grainfmt): Allow directory input & output (#1274)
Browse files Browse the repository at this point in the history
feat(grainfmt)!: Replace `--in-place` flag with `-o` flag
chore(grainfmt)!: Remove stdin formatting support
fix(cli): Ensure parent flags are inherited by the format command
chore(cli): Update grain doc command
chore: Update doc strings
chore(stdlib): Format the Regex module
  • Loading branch information
phated authored May 29, 2022
1 parent acec7ff commit d3e7a33
Show file tree
Hide file tree
Showing 6 changed files with 1,923 additions and 938 deletions.
8 changes: 5 additions & 3 deletions cli/bin/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ function execGrainformat(
execOpts = { stdio: "pipe" }
) {
const flags = [];
const options = program.opts();
program.options.forEach((option) => {
// Inherit compiler flags passed to the parent
const options = program.parent.options.concat(program.options);
const opts = { ...program.parent.opts(), ...program.opts() };
options.forEach((option) => {
if (!option.forward) return;
const flag = option.toFlag(options);
const flag = option.toFlag(opts);
if (flag) flags.push(flag);
});

Expand Down
5 changes: 2 additions & 3 deletions cli/bin/grain.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ program
);

program
.command("doc <file>")
.command("doc <file|dir>")
.description("generate documentation for a grain file")
.forwardOption(
"--current-version <version>",
Expand All @@ -220,9 +220,8 @@ program
);

program
.command("format [file]")
.command("format <file|dir>")
.description("format a grain file")
.forwardOption("--in-place", "format in place")
.action(
wrapAction(function (file, options, program) {
format(file, program);
Expand Down
4 changes: 2 additions & 2 deletions compiler/graindoc/graindoc.re
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ let () =

[@deriving cmdliner]
type io_params = {
/** Grain source file for which to extract documentation */
/** Grain source file or directory of source files to document */
[@pos 0] [@docv "FILE"]
input: ExistingFileOrDirectory.t,
/** Output filename */
/** Output file or directory */
[@name "o"] [@docv "FILE"]
output: option(MaybeExistingFileOrDirectory.t),
};
Expand Down
2 changes: 2 additions & 0 deletions compiler/grainformat/dune
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
(:include ./config/flags.sexp)))
(libraries cmdliner grain grain_utils grain_parsing grainformat.format
binaryen dune-build-info)
(preprocess
(pps ppx_deriving_cmdliner))
(js_of_ocaml
(flags --no-sourcemap --no-extern-fs --quiet --disable share)))
210 changes: 104 additions & 106 deletions compiler/grainformat/grainformat.re
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,29 @@ open Grain;
open Compile;
open Grain_parsing;
open Grain_utils;
open Filename;
open Grain_utils.Filepath.Args;

[@deriving cmdliner]
type io_params = {
/** Grain source file or directory of source files to format */
[@pos 0] [@docv "FILE"]
input: ExistingFileOrDirectory.t,
/** Output file or directory */
[@name "o"] [@docv "FILE"]
output: option(MaybeExistingFileOrDirectory.t),
};

let get_program_string = filename => {
switch (filename) {
| None =>
let source_buffer = Buffer.create(1024);
set_binary_mode_in(stdin, true);
/* read from stdin until we get end of buffer */
try(
while (true) {
let c = input_char(stdin);
Buffer.add_char(source_buffer, c);
}
) {
| exn => ()
};
Buffer.contents(source_buffer);
| Some(filename) =>
let ic = open_in_bin(filename);
let n = in_channel_length(ic);
let source_buffer = Buffer.create(n);
Buffer.add_channel(source_buffer, ic, n);
close_in(ic);
Buffer.contents(source_buffer);
};
let ic = open_in_bin(filename);
let n = in_channel_length(ic);
let source_buffer = Buffer.create(n);
Buffer.add_channel(source_buffer, ic, n);
close_in(ic);
Buffer.contents(source_buffer);
};

let compile_parsed = (filename: option(string)) => {
let compile_parsed = filename => {
let filename = Filepath.to_string(filename);
switch (
{
let program_str = get_program_string(filename);
Expand All @@ -42,7 +37,7 @@ let compile_parsed = (filename: option(string)) => {
Compile.compile_string(
~is_root_file=true,
~hook=stop_after_parse,
~name=?filename,
~name=filename,
program_str,
);

Expand All @@ -59,110 +54,123 @@ let compile_parsed = (filename: option(string)) => {
Grain_parsing.Location.report_exception(Stdlib.Format.err_formatter, exn);
Option.iter(
s =>
if (Grain_utils.Config.debug^) {
if (Config.debug^) {
prerr_string("Backtrace:\n");
prerr_string(s);
prerr_string("\n");
},
bt,
);
exit(2);
| ({cstate_desc: Parsed(parsed_program)}, lines, eol) =>
`Ok((parsed_program, Array.of_list(lines), eol))
| _ => `Error((false, "Invalid compilation state"))
| ({cstate_desc: Parsed(parsed_program)}, lines, eol) => (
parsed_program,
Array.of_list(lines),
eol,
)
| _ => failwith("Invalid compilation state")
};
};

let format_code =
(
~eol,
srcfile: option(string),
~output=?,
~original_source: array(string),
program: Parsetree.parsed_program,
outfile,
original_source: array(string),
format_in_place: bool,
) => {
let formatted_code = Format.format_ast(~original_source, ~eol, program);

// return the file to its format

let buf = Buffer.create(0);
Buffer.add_string(buf, formatted_code);

let contents = Buffer.to_bytes(buf);
switch (outfile) {
switch (output) {
| Some(outfile) =>
let outfile = Filepath.to_string(outfile);
// TODO: This crashes if you do something weird like `-o stdout/map.gr/foo`
// because `foo` doesn't exist so it tries to mkdir it and raises
Fs_access.ensure_parent_directory_exists(outfile);
let oc = Fs_access.open_file_for_writing(outfile);
output_bytes(oc, contents);
close_out(oc);
| None =>
switch (srcfile, format_in_place) {
| (Some(src), true) =>
let oc = Fs_access.open_file_for_writing(src);
output_bytes(oc, contents);
close_out(oc);
| _ =>
set_binary_mode_out(stdout, true);
print_bytes(contents);
}
set_binary_mode_out(stdout, true);
print_bytes(contents);
};

`Ok();
};

let grainformat =
(
srcfile: option(string),
outfile,
format_in_place: bool,
(program, lines: array(string), eol),
) =>
try(format_code(~eol, srcfile, program, outfile, lines, format_in_place)) {
| e => `Error((false, Printexc.to_string(e)))
};

let input_file_conv = {
open Arg;
let (prsr, prntr) = non_dir_file;
(filename => prsr(filename), prntr);
};

/** Converter which checks that the given output filename is valid */
let output_file_conv = {
let parse = s => {
let s_dir = dirname(s);
Sys.file_exists(s_dir)
? if (Sys.is_directory(s_dir)) {
`Ok(s);
} else {
`Error(Stdlib.Format.sprintf("`%s' is not a directory", s_dir));
}
: `Error(Stdlib.Format.sprintf("no `%s' directory", s_dir));
};
(parse, Stdlib.Format.pp_print_string);
type run = {
input_path: Fp.t(Fp.absolute),
output_path: option(Fp.t(Fp.absolute)),
};

let output_filename = {
let doc = "Output filename";
let docv = "FILE";
Arg.(
value & opt(some(output_file_conv), None) & info(["o"], ~docv, ~doc)
let enumerate_directory = (input_dir_path, output_dir_path) => {
let all_files = Array.to_list(Fs_access.readdir(input_dir_path));
let grain_files =
List.filter(
filepath => Filename.extension(Fp.toString(filepath)) == ".gr",
all_files,
);
List.map(
filepath => {
// We relativize between the input directory and the full filepath
// such that we can reconstruct the directory structure of the input directory
let relative_path =
Fp.relativizeExn(~source=input_dir_path, ~dest=filepath);
let gr_basename = Option.get(Fp.baseName(relative_path));
let dirname = Fp.dirName(relative_path);
let md_relative_path = Fp.join(dirname, Fp.relativeExn(gr_basename));
let output_path = Fp.join(output_dir_path, md_relative_path);
{input_path: filepath, output_path: Some(output_path)};
},
grain_files,
);
};

let format_in_place = {
let doc = "Format in place";
let docv = "";
Arg.(value & flag & info(["in-place"], ~docv, ~doc));
};
let enumerate_runs = opts =>
switch (opts.input, opts.output) {
| (File(input_file_path), None) =>
`Ok([{input_path: input_file_path, output_path: None}])
| (File(input_file_path), Some(Exists(File(output_file_path)))) =>
`Ok([
{input_path: input_file_path, output_path: Some(output_file_path)},
])
| (File(input_file_path), Some(NotExists(output_file_path))) =>
`Ok([
{input_path: input_file_path, output_path: Some(output_file_path)},
])
| (Directory(_), None) =>
`Error((
false,
"Directory input must be used with `-o` flag to specify output directory",
))
| (Directory(input_dir_path), Some(Exists(Directory(output_dir_path)))) =>
`Ok(enumerate_directory(input_dir_path, output_dir_path))
| (Directory(input_dir_path), Some(NotExists(output_dir_path))) =>
`Ok(enumerate_directory(input_dir_path, output_dir_path))
| (File(input_file_path), Some(Exists(Directory(output_dir_path)))) =>
`Error((
false,
"Using a file as input cannot be combined with directory output",
))
| (Directory(_), Some(Exists(File(_)))) =>
`Error((
false,
"Using a directory as input cannot be written as a single file output",
))
};

let input_filename = {
let doc = "Grain source file to format";
let docv = "FILE";
Arg.(
value
& pos(~rev=true, 0, some(~none="", input_file_conv), None)
& info([], ~docv, ~doc)
let grainformat = runs => {
List.iter(
({input_path, output_path}) => {
let (program, original_source, eol) = compile_parsed(input_path);
try(format_code(~eol, ~output=?output_path, ~original_source, program)) {
| exn =>
Stdlib.Format.eprintf("@[%s@]@.", Printexc.to_string(exn));
exit(2);
};
},
runs,
);
};

Expand All @@ -178,18 +186,8 @@ let cmd = {

Cmd.v(
Cmd.info(Sys.argv[0], ~version, ~doc),
Term.(
ret(
const(grainformat)
$ input_filename
$ output_filename
$ format_in_place
$ ret(
Grain_utils.Config.with_cli_options(compile_parsed)
$ input_filename,
),
)
),
Config.with_cli_options(grainformat)
$ ret(const(enumerate_runs) $ io_params_cmdliner_term()),
);
};

Expand Down
Loading

0 comments on commit d3e7a33

Please sign in to comment.