diff --git a/infer/lib/erlang/erlang.escript b/infer/lib/erlang/erlang.escript new file mode 100755 index 000000000..9ce43249a --- /dev/null +++ b/infer/lib/erlang/erlang.escript @@ -0,0 +1,146 @@ +#!/usr/bin/env escript +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. +% +% +% Usage: +% erlang.escript [ast_out_dir] -- rebar3 [args ...] +% erlang.escript [ast_out_dir] -- erlc [args ...] +% +% This script produces a bash command that makes rebar3 or erlc +% to execute with [args ...], and in addition to write the JSON +% representation of the Erlang AST for each file compiled +% in [ast_out_dir] or - if not provided - in the build +% directory next to the corresponding compiled beam. + +main([]) -> + usage(); +main(Args) -> + {SArgs, Cmd} = split_args(Args), + OutDir = + case SArgs of + [] -> false; + [Dir] -> Dir; + _ -> usage() + end, + ScriptDir = filename:dirname(escript:script_name()), + ParseTransformDir = filename:join(ScriptDir, "infer_parse_transform"), + case run("rebar3 compile", ParseTransformDir) of + 0 -> + ok; + ExitStatus -> + io:format("error: `rebar3 compile` in `~s` returned exit code ~p~n", [ + ParseTransformDir, + ExitStatus + ]), + halt(1) + end, + LibPath = filename:join(ParseTransformDir, "_build/default/lib"), + case Cmd of + ["rebar3" | _] -> + rebar3(LibPath, OutDir, Cmd); + ["erlc" | _] -> + erlc(LibPath, OutDir, Cmd); + _ -> + io:format("error: unrecognized command ~s~n", [string:join(Cmd, " ")]), + halt(1) + end. + +usage() -> + io:format("valid arguments:~n"), + io:format(" [ast_out_dir] -- rebar3 [args] ...~n"), + io:format(" [ast_out_dir] -- erlc [args] ...~n"), + halt(1). + +load_config_from_list([]) -> + false; +load_config_from_list([H | T]) -> + case load_config(H) of + {ok, Config} -> Config; + _ -> load_config_from_list(T) + end. + +load_config(ConfigPath) when is_list(ConfigPath) -> + case lists:suffix(".script", ConfigPath) of + true -> + BaseConfigPath = filename:rootname(ConfigPath, ".script"), + BaseConfig = + case load_config(BaseConfigPath) of + {ok, Config} -> Config; + _ -> [] + end, + file:script(ConfigPath, [{'CONFIG', BaseConfig}, {'SCRIPT', ConfigPath}]); + false -> + file:consult(ConfigPath) + end; +load_config(_) -> + false. + +split_args(Args) -> + try + split_args_rec(Args, []) + catch + _:_ -> usage() + end. + +split_args_rec(["--" | RebarCmd], Args) -> {Args, RebarCmd}; +split_args_rec([H | T], Args) -> split_args_rec(T, Args ++ [H]). + +run(Command, Dir) -> + Port = erlang:open_port( + {spawn, Command}, + [exit_status, {cd, Dir}] + ), + receive + {Port, {exit_status, Status}} -> Status + end. + +rebar3(LibPath, OutDir, Cmd) -> + ConfigPaths = [os:getenv("REBAR_CONFIG"), "rebar.config.script", "rebar.config"], + Original = + case load_config_from_list(ConfigPaths) of + false -> + io:format("error: no rebar3 config found~n"), + halt(1); + Config -> + Config + end, + Altered = inject_parse_transform(Original, OutDir), + AltConfigPath = string:trim(os:cmd("mktemp --suffix .script")), + file:write_file(AltConfigPath, io_lib:fwrite("~p.~n", [Altered])), + + io:format("ERL_LIBS=\"~s:$ERL_LIBS\" REBAR_CONFIG=\"~s\" ~s~n", [ + LibPath, + AltConfigPath, + string:join(Cmd, " ") + ]). + +erlc(LibPath, OutDir, Cmd) -> + [{erl_opts, Options}] = inject_parse_transform([], OutDir), + OptionList = ["+'" ++ io_lib:format("~p", [Item]) ++ "'" || Item <- Options], + [ErlC | Args] = Cmd, + io:format("ERL_LIBS=\"~s:$ERL_LIBS\" ~s ~s ~s~n", [ + LibPath, + ErlC, + string:join(OptionList, " "), + string:join(Args, " ") + ]). + +inject_parse_transform(Original, OutDir) -> + ErlOpts = + case lists:keyfind(erl_opts, 1, Original) of + {erl_opts, Opts} -> Opts; + false -> [] + end, + ErlOpts1 = + ErlOpts ++ + [{parse_transform, infer_parse_transform}] ++ + if + OutDir =/= false -> + [{ast_outdir, OutDir}]; + true -> + [] + end, + lists:keystore(erl_opts, 1, Original, {erl_opts, ErlOpts1}). diff --git a/infer/lib/erlang/erlang.sh b/infer/lib/erlang/erlang.sh new file mode 100755 index 000000000..1686d8d01 --- /dev/null +++ b/infer/lib/erlang/erlang.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +set -o pipefail +set -u + +basedir=$(dirname "${BASH_SOURCE[0]}") +out=$("${basedir}"/erlang.escript "$@") +exit_code=$? +echo "$out" +if [ $exit_code = 0 ]; then + sh -c "$out" + exit $? +fi +exit $exit_code diff --git a/infer/lib/erlang/infer_parse_transform/rebar.config b/infer/lib/erlang/infer_parse_transform/rebar.config new file mode 100644 index 000000000..88e0d450e --- /dev/null +++ b/infer/lib/erlang/infer_parse_transform/rebar.config @@ -0,0 +1,9 @@ +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. + +{erl_opts, []}. +{deps, [ + {jsone, {git, "https://github.com/sile/jsone.git", {tag, "1.5.6"}}} +]}. diff --git a/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.app.src b/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.app.src new file mode 100644 index 000000000..e3fb82c8d --- /dev/null +++ b/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.app.src @@ -0,0 +1,16 @@ +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. + +{application, infer_parse_transform, + [{description, "parse_transform to dump the Erlang AST as a JSON structure"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, + [kernel, + stdlib, + jsone + ]}, + {env,[]} + ]}. diff --git a/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.erl b/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.erl new file mode 100644 index 000000000..a295f6371 --- /dev/null +++ b/infer/lib/erlang/infer_parse_transform/src/infer_parse_transform.erl @@ -0,0 +1,50 @@ +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. + +-module(infer_parse_transform). + +-export([parse_transform/2]). + +-type forms() :: erl_parse:abstract_form() | erl_parse:form_info(). + +-spec parse_transform([forms()], [compile:option()]) -> [forms()]. +parse_transform(Forms, Options) -> + OutDir = + case lists:keyfind(ast_outdir, 1, Options) of + {ast_outdir, Dir} -> + Dir; + _ -> + case lists:keyfind(outdir, 1, Options) of + {outdir, Dir} -> Dir; + _ -> error(abort) + end + end, + FileName = + case lists:keyfind(file, 3, Forms) of + {attribute, _, file, {FilePath, _}} -> filename:basename(FilePath); + _ -> error(abort) + end, + dump_ast_as_json(OutDir, FileName, Forms), + Forms. + +dump_ast_as_json(Dir, SourceFileName, Forms) -> + FileName = filename:rootname(SourceFileName) ++ ".json", + FilePath = filename:join(Dir, FileName), + Object = ast_to_json(Forms), + Contents = jsone:encode(Object), + file:write_file(FilePath, Contents). + +ast_to_json([]) -> + []; +ast_to_json(Node) when is_list(Node) -> + case lists:all(fun(Item) -> is_integer(Item) end, Node) of + true -> unicode:characters_to_binary(Node); + false -> [ast_to_json(Child) || Child <- Node] + end; +ast_to_json(Node) when is_tuple(Node) -> + L = tuple_to_list(Node), + ast_to_json(L); +ast_to_json(Node) -> + Node.