%% =====================================================================
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License as
%% published by the Free Software Foundation; either version 2 of the
%% License, or (at your option) any later version.
%%
%% This library is distributed in the hope that it will be useful, but
%% WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%% Lesser General Public License for more details.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA
%%
%% $Id$
%%
%% @copyright 1998-2006 Richard Carlsson
%% @author Richard Carlsson <[email protected]>
%% @end
%% =====================================================================
%% @doc Igor: the Module Merger and Renamer.
%%
%% The program Igor merges the source code of one or more Erlang
%% modules into a single module, which can then replace the original set
%% of modules. Igor is also able to rename a set of (possibly
%% interdependent) modules, without joining them into a single
%% module.
%%
%% The main user interface consists of the functions {@link merge/3} and
%% {@link rename/3}. See also the function {@link parse_transform/2}.
%%
%% A note of warning: Igor cannot do anything about the case when the
%% name of a remote function is passed to the built-in functions
%% `apply' and `spawn' <em>unless</em> the module
%% and function names are explicitly stated in the call, as in e.g.
%% `apply(lists, reverse, [Xs])'. In all other cases, Igor
%% leaves such calls unchanged, and warns the user that manual editing
%% might be necessary.
%%
%% Also note that Erlang records will be renamed as necessary to
%% avoid non-equivalent definitions using the same record name. This
%% does not work if the source code accesses the name field of such
%% record tuples by `element/2' or similar methods. Always
%% use the record syntax to handle record tuples, if possible.
%%
%% Disclaimer: the author of this program takes no responsibility for
%% the correctness of the produced output, or for any effects of its
%% execution. In particular, the author may not be held responsible
%% should Igor include the code of a deceased madman in the result.
%%
%% For further information on Igors in general, see e.g. "Young
%% Frankenstein", Mel Brooks, 1974, and "The Fifth Elephant", Terry
%% Pratchett, 1999.
%% @end
%% =====================================================================
%% This program is named after the character Igor, assistant to Dr.
%% Frankenstein, in the 1939 film "Son of Frankenstein" (with Boris
%% Karloff playing The Monster for the last time; Igor was played by
%% Bela Lugosi). Igor's job (in the film) was mainly to bring reasonably
%% fresh parts of various human corpses to the good Doctor, for his
%% purpose of reanimating them in the shape of a new formidable, living
%% creature.
%%
%% Merging code is done by joining the sources, possibly changing the
%% order of declarations as necessary, renaming functions and records to
%% avoid name clashes, and changing remote calls to local calls where
%% possible. Stub modules may be automatically generated to redirect any
%% calls that still use the old names. Indirectly, code merging can be
%% used to simply rename a set of modules.
%%
%% What Igor does not do is to optimise the resulting code, which
%% typically can benefit from techniques such as inlining, constant
%% folding, specialisation, etc. This task is left to the Doctor.
%% (Luckily, Igor can call on Inga to do some cleanup; cf. 'erl_tidy'.)
%% TODO: FIXME: don't remove module qualifier if name is (auto-)imported!
%% TODO: handle merging of parameterized modules (somehow).
%% TODO: check for redefinition of macros; check equivalence; comment out.
%% TODO: {export, [E]}, E = atom() | {atom(), atom(), integer()}.
%% TODO: improve documentation.
%% TODO: optionally rename all functions from specified (or all) modules.
-module(igor).
-export([create_stubs/2, merge/2, merge/3, merge_files/3, merge_files/4,
merge_sources/3, parse_transform/2, rename/2, rename/3]).
-include_lib("kernel/include/file.hrl").
%% =====================================================================
%% Global Constants
-define(NOTE_HEADER, "Note from Igor: ").
-define(COMMENT_PREFIX, "% ").
-define(COMMENT_BAR,
"======================="
"======================="
"=======================").
-define(NOTE_PREFIX, "%! ").
-define(KILL_PREFIX, "%<<< ").
-define(DEFAULT_INCLUDES, ["."]).
-define(DEFAULT_MACROS, []).
-define(DEFAULT_SUFFIX, ".erl").
-define(DEFAULT_BACKUP_SUFFIX, ".bak").
-define(DEFAULT_DIR, "").
-define(DEFAULT_STUB_DIR, "stubs").
-define(TIDY_OPTS, [quiet]).
%% This may also be used in patterns. R must not be an integer, i.e.,
%% the structure must be distinct from function names.
-define(record_name(R), {record, R}).
%% =====================================================================
-type ordset(X) :: [X]. % XXX: TAKE ME OUT
%% =====================================================================
%% Data structure for module information
-record(module, {name :: atom(),
vars = none :: [atom()] | 'none',
functions :: ordset({atom(), arity()}),
exports :: ordset({atom(), arity()})
| ordset({{atom(), arity()}, term()}),
aliases :: ordset({{atom(), arity()},
{atom(), {atom(), arity()}}}),
attributes :: ordset({atom(), term()}),
records :: [{atom(), [{atom(), term()}]}]
}).
%% The default pretty-printing function.
default_printer(Tree, Options) ->
erl_prettypr:format(Tree, Options).
%% =====================================================================
-type option() :: atom() | {atom(), term()}.
-type attribute() :: {atom(), term()}.
-type moduleName() :: atom().
-type functionName() :: {atom(), arity()}.
-type functionPair() :: {functionName(), {moduleName(), functionName()}}.
-type stubDescriptor() :: [{moduleName(), [functionPair()], [attribute()]}].
-type notes() :: 'always' | 'yes' | 'no'.
%% =====================================================================
%% @spec parse_transform(Forms::[syntaxTree()], Options::[term()]) ->
%% [syntaxTree()]
%%
%% syntaxTree() = erl_syntax:syntaxTree()
%%
%% @doc Allows Igor to work as a component of the Erlang compiler.
%% Including the term `{parse_transform, igor}' in the
%% compile options when compiling an Erlang module (cf.
%% `compile:file/2'), will call upon Igor to process the
%% source code, allowing automatic inclusion of other source files. No
%% files are created or overwritten when this function is used.
%%
%% Igor will look for terms `{igor, List}' in the compile
%% options, where `List' is a list of Igor-specific options,
%% as follows:
%% <dl>
%% <dt>`{files, [filename()]}'</dt>
%% <dd>The value specifies a list of source files to be merged with
%% the file being compiled; cf. `merge_files/4'.</dd>
%% </dl>
%%
%% See `merge_files/4' for further options. Note, however,
%% that some options are preset by this function and cannot be
%% overridden by the user; in particular, all cosmetic features are
%% turned off, for efficiency. Preprocessing is turned on.
%%
%% @see merge_files/4
%% @see //compiler/compile:file/2
-spec parse_transform(erl_syntax:forms(), [option()]) ->
[erl_syntax:syntaxTree()].
parse_transform(Forms, Options) ->
M = get_module_info(Forms),
Name = M#module.name,
Opts = proplists:append_values(igor, Options),
Files = proplists:append_values(files, Opts),
%% We turn off all features that are only cosmetic, and make sure to
%% turn on preservation of `file' attributes.
Opts1 = [{comments, false},
{notes, no},
{no_imports, true},
{file_attributes, yes},
{preprocess, true},
{export, [Name]}
| Opts],
{T, _} = merge_files(Name, [Forms], Files, Opts1),
verbose("done.", Opts1),
erl_syntax:revert_forms(T).
%% =====================================================================
%% @spec merge(Name::atom(), Files::[filename()]) -> [filename()]
%% @equiv merge(Name, Files, [])
-spec merge(atom(), [file:filename()]) -> [file:filename()].
merge(Name, Files) ->
merge(Name, Files, []).
%% =====================================================================
%% @spec merge(Name::atom(), Files::[filename()], Options::[term()]) ->
%% [filename()]
%%
%% filename() = file:filename()
%%
%% @doc Merges source code files to a single file. `Name'
%% specifies the name of the resulting module - not the name of the
%% output file. `Files' is a list of file names and/or module
%% names of source modules to be read and merged (see
%% `merge_files/4' for details). All the input modules must
%% be distinctly named.
%%
%% The resulting source code is written to a file named
%% "`<em>Name</em>.erl'" in the current directory, unless
%% otherwise specified by the options `dir' and
%% `outfile' described below.
%%
%% Examples:
%% <ul>
%% <li>given a module `m' in file "`m.erl'"
%% which uses the standard library module `lists', calling
%% `igor:merge(m, [m, lists])' will create a new file
%% "`m.erl' which contains the code from `m' and
%% exports the same functions, and which includes the referenced code
%% from the `lists' module. The original file will be
%% renamed to "`m.erl.bak'".</li>
%%
%% <li>given modules `m1' and `m2', in
%% corresponding files, calling `igor:merge(m, [m1, m2])'
%% will create a file "`m.erl'" which contains the code
%% from `m1' and `m2' and exports the functions
%% of `m1'.</li>
%% </ul>
%%
%% Stub module files are created for those modules that are to be
%% exported by the target module (see options `export',
%% `stubs' and `stub_dir').
%%
%% The function returns the list of file names of all created
%% modules, including any automatically created stub modules. The file
%% name of the target module is always first in the list.
%%
%% Note: If you get a "syntax error" message when trying to merge
%% files (and you know those files to be correct), then try the
%% `preprocess' option. It typically means that your code
%% contains too strange macros to be handled without actually performing
%% the preprocessor expansions.
%%
%% Options:
%% <dl>
%% <dt>`{backup_suffix, string()}'</dt>
%%
%% <dd>Specifies the file name suffix to be used when a backup file
%% is created; the default value is `".bak"'.</dd>
%%
%% <dt>`{backups, boolean()}'</dt>
%%
%% <dd>If the value is `true', existing files will be
%% renamed before new files are opened for writing. The new names
%% are formed by appending the string given by the
%% `backup_suffix' option to the original name. The
%% default value is `true'.</dd>
%%
%% <dt>`{dir, filename()}'</dt>
%%
%% <dd>Specifies the name of the directory in which the output file
%% is to be written. An empty string is interpreted as the current
%% directory. By default, the current directory is used.</dd>
%%
%% <dt>`{outfile, filename()}'</dt>
%%
%% <dd>Specifies the name of the file (without suffix) to which the
%% resulting source code is to be written. By default, this is the
%% same as the `Name' argument.</dd>
%%
%% <dt>`{preprocess, boolean()}'</dt>
%%
%% <dd>If the value is `true', preprocessing will be done
%% when reading the source code. See `merge_files/4' for
%% details.</dd>
%%
%% <dt>`{printer, Function}'</dt>
%% <dd><ul>
%% <li>`Function = (syntaxTree()) -> string()'</li>
%% </ul>
%% Specifies a function for prettyprinting Erlang syntax trees.
%% This is used for outputting the resulting module definition, as
%% well as for creating stub files. The function is assumed to
%% return formatted text for the given syntax tree, and should raise
%% an exception if an error occurs. The default formatting function
%% calls `erl_prettypr:format/2'.</dd>
%%
%% <dt>`{stub_dir, filename()}'</dt>
%%
%% <dd>Specifies the name of the directory to which any generated
%% stub module files are written. The default value is
%% `"stubs"'.</dd>
%%
%% <dt>`{stubs, boolean()}'</dt>
%%
%% <dd>If the value is `true', stub module files will be
%% automatically generated for all exported modules that do not have
%% the same name as the target module. The default value is
%% `true'.</dd>
%%
%% <dt>`{suffix, string()}'</dt>
%%
%% <dd>Specifies the suffix to be used for the output file names;
%% the default value is `".erl"'.</dd>
%% </dl>
%%
%% See `merge_files/4' for further options.
%%
%% @see merge/2
%% @see merge_files/4
%% The defaults for 'merge' are also used for 'create_stubs'.
-define(DEFAULT_MERGE_OPTS,
[{backup_suffix, ?DEFAULT_BACKUP_SUFFIX},
backups,
{dir, ?DEFAULT_DIR},
{printer, fun default_printer/2},
{stub_dir, ?DEFAULT_STUB_DIR},
stubs,
{suffix, ?DEFAULT_SUFFIX},
{verbose, false}]).
-spec merge(atom(), [file:filename()], [option()]) -> [file:filename()].
merge(Name, Files, Opts) ->
Opts1 = Opts ++ ?DEFAULT_MERGE_OPTS,
{Tree, Stubs} = merge_files(Name, Files, Opts1),
Dir = proplists:get_value(dir, Opts1, ""),
Filename = proplists:get_value(outfile, Opts1, Name),
File = write_module(Tree, Filename, Dir, Opts1),
[File | maybe_create_stubs(Stubs, Opts1)].
%% =====================================================================
%% @spec merge_files(Name::atom(), Files::[filename()],
%% Options::[term()]) ->
%% {syntaxTree(), [stubDescriptor()]}
%% @equiv merge_files(Name, [], Files, Options)
-spec merge_files(atom(), [file:filename()], [option()]) ->
{erl_syntax:syntaxTree(), [stubDescriptor()]}.
merge_files(Name, Files, Options) ->
merge_files(Name, [], Files, Options).
%% =====================================================================
%% @spec merge_files(Name::atom(), Sources::[Forms],
%% Files::[filename()], Options::[term()]) ->
%% {syntaxTree(), [stubDescriptor()]}
%% Forms = syntaxTree() | [syntaxTree()]
%%
%% @doc Merges source code files and syntax trees to a single syntax
%% tree. This is a file-reading front end to
%% `merge_sources/3'. `Name' specifies the name of
%% the resulting module - not the name of the output file.
%% `Sources' is a list of syntax trees and/or lists of
%% "source code form" syntax trees, each entry representing a module
%% definition. `Files' is a list of file names and/or module
%% names of source modules to be read and included. All the input
%% modules must be distinctly named.
%%
%% If a name in `Files' is not the name of an existing
%% file, Igor assumes it represents a module name, and tries to locate
%% and read the corresponding source file. The parsed files are appended
%% to `Sources' and passed on to
%% `merge_sources/3', i.e., entries in `Sources'
%% are listed before entries read from files.
%%
%% If no exports are listed by an `export' option (see
%% `merge_sources/3' for details), then if `Name'
%% is also the name of one of the input modules, that module will be
%% exported; otherwise, the first listed module will be exported. Cf.
%% the examples under `merge/3'.
%%
%% The result is a pair `{Tree, Stubs}', where
%% `Tree' represents the source code that is the result of
%% merging all the code in `Sources' and `Files',
%% and `Stubs' is a list of stub module descriptors (see
%% `merge_sources/3' for details).
%%
%% Options:
%% <dl>
%% <dt>`{comments, boolean()}'</dt>
%%
%% <dd>If the value is `true', source code comments in
%% the original files will be preserved in the output. The default
%% value is `true'.</dd>
%%
%% <dt>`{find_src_rules, [{string(), string()}]}'</dt>
%%
%% <dd>Specifies a list of rules for associating object files with
%% source files, to be passed to the function
%% `filename:find_src/2'. This can be used to change the
%% way Igor looks for source files. If this option is not specified,
%% the default system rules are used. The first occurrence of this
%% option completely overrides any later in the option list.</dd>
%%
%% <dt>`{includes, [filename()]}'</dt>
%%
%% <dd>Specifies a list of directory names for the Erlang
%% preprocessor, if used, to search for include files (cf. the
%% `preprocess' option). The default value is the empty
%% list. The directory of the source file and the current directory
%% are automatically appended to the list.</dd>
%%
%% <dt>`{macros, [{atom(), term()}]}'</dt>
%%
%% <dd>Specifies a list of "pre-defined" macro definitions for the
%% Erlang preprocessor, if used (cf. the `preprocess'
%% option). The default value is the empty list.</dd>
%%
%% <dt>`{preprocess, boolean()}'</dt>
%%
%% <dd>If the value is `false', Igor will read source
%% files without passing them through the Erlang preprocessor
%% (`epp'), in order to avoid expansion of preprocessor
%% directives such as `-include(...).',
%% `-define(...).' and `-ifdef(...)', and
%% macro calls such as `?LINE' and `?MY_MACRO(x,
%% y)'. The default value is `false', i.e.,
%% preprocessing is not done. (See the module
%% `epp_dodger' for details.)
%%
%% Notes: If a file contains too exotic definitions or uses of
%% macros, it will not be possible to read it without preprocessing.
%% Furthermore, Igor does not currently try to sort out multiple
%% inclusions of the same file, or redefinitions of the same macro
%% name. Therefore, when preprocessing is turned off, it may become
%% necessary to edit the resulting source code, removing such
%% re-inclusions and redefinitions.</dd>
%% </dl>
%%
%% See `merge_sources/3' for further options.
%%
%% @see merge/3
%% @see merge_files/3
%% @see merge_sources/3
%% @see //stdlib/filename:find_src/2
%% @see epp_dodger
-spec merge_files(atom(), erl_syntax:forms(), [file:filename()], [option()]) ->
{erl_syntax:syntaxTree(), [stubDescriptor()]}.
merge_files(_, _Trees, [], _) ->
report_error("no files to merge."),
exit(badarg);
merge_files(Name, Trees, Files, Opts) ->
Opts1 = Opts ++ [{includes, ?DEFAULT_INCLUDES},
{macros, ?DEFAULT_MACROS},
{preprocess, false},
comments],
Sources = [read_module(F, Opts1) || F <- Files],
merge_sources(Name, Trees ++ Sources, Opts1).
%% =====================================================================
%% @spec merge_sources(Name::atom(), Sources::[Forms],
%% Options::[term()]) ->
%% {syntaxTree(), [stubDescriptor()]}
%%
%% Forms = syntaxTree() | [syntaxTree()]
%%
%% @type stubDescriptor() = [{ModuleName, Functions, [Attribute]}]
%% ModuleName = atom()
%% Functions = [{FunctionName, {ModuleName, FunctionName}}]
%% FunctionName = {atom(), integer()}
%% Attribute = {atom(), term()}.
%%
%% A stub module descriptor contains the module name, a list of
%% exported functions, and a list of module attributes. Each
%% function is described by its name (which includes its arity),
%% and the corresponding module and function that it calls. (The
%% arities should always match.) The attributes are simply
%% described by key-value pairs.
%%
%% @doc Merges syntax trees to a single syntax tree. This is the main
%% code merging "engine". `Name' specifies the name of the
%% resulting module. `Sources' is a list of syntax trees of
%% type `form_list' and/or lists of "source code form" syntax
%% trees, each entry representing a module definition. All the input
%% modules must be distinctly named.
%%
%% Unless otherwise specified by the options, all modules are assumed
%% to be at least "static", and all except the target module are assumed
%% to be "safe". See the `static' and `safe'
%% options for details.
%%
%% If `Name' is also the name of one of the input modules,
%% the code from that module will occur at the top of the resulting
%% code, and no extra "header" comments will be added. In other words,
%% the look of that module will be preserved.
%%
%% The result is a pair `{Tree, Stubs}', where
%% `Tree' represents the source code that is the result of
%% merging all the code in `Sources', and `Stubs'
%% is a list of stub module descriptors (see below).
%%
%% `Stubs' contains one entry for each exported input
%% module (cf. the `export' option), each entry describing a
%% stub module that redirects calls of functions in the original module
%% to the corresponding (possibly renamed) functions in the new module.
%% The stub descriptors can be used to automatically generate stub
%% modules; see `create_stubs/2'.
%%
%% Options:
%% <dl>
%% <dt>`{export, [atom()]}'</dt>
%%
%% <dd>Specifies a list of names of input modules whose interfaces
%% should be exported by the output module. A stub descriptor is
%% generated for each specified module, unless its name is
%% `Name'. If no modules are specified, then if
%% `Name' is also the name of an input module, that
%% module will be exported; otherwise the first listed module in
%% `Sources' will be exported. The default value is the
%% empty list.</dd>
%%
%% <dt>`{export_all, boolean()}'</dt>
%%
%% <dd>If the value is `true', this is equivalent to
%% listing all of the input modules in the `export'
%% option. The default value is `false'.</dd>
%%
%% <dt>`{file_attributes, Preserve}'</dt>
%% <dd><ul>
%% <li>`Preserve = yes | comment | no'</li>
%% </ul>
%% If the value is `yes', all file attributes
%% `-file(...)' in the input sources will be preserved in
%% the resulting code. If the value is `comment', they
%% will be turned into comments, but remain in their original
%% positions in the code relative to the other source code forms. If
%% the value is `no', all file attributes will be removed
%% from the code, unless they have attached comments, in which case
%% they will be handled as in the `comment' case. The
%% default value is `no'.</dd>
%%
%% <dt>`{no_banner, boolean()}'</dt>
%%
%% <dd>If the value is `true', no banner comment will be
%% added at the top of the resulting module, even if the target
%% module does not have the same name as any of the input modules.
%% Instead, Igor will try to preserve the look of the module whose
%% code is at the top of the output. The default value is
%% `false'.</dd>
%%
%% <dt>`{no_headers, boolean()}'</dt>
%%
%% <dd>If the value is `true', no header comments will be
%% added to the resulting module at the beginning of each section of
%% code that originates from a particular input module. The default
%% value is `false', which means that section headers are
%% normally added whenever more than two or more modules are
%% merged.</dd>
%%
%% <dt>`{no_imports, boolean()}'</dt>
%%
%% <dd>If the value is `true', all
%% `-import(...)' declarations in the original code will
%% be expanded in the result; otherwise, as much as possible of the
%% original import declarations will be preserved. The default value
%% is `false'.</dd>
%%
%% <dt>`{notes, Notes}'</dt>
%% <dd><ul>
%% <li>`Notes = always | yes | no'</li>
%% </ul>
%% If the value is `yes', comments will be inserted where
%% important changes have been made in the code. If the value is
%% `always', <em>all</em> changes to the code will be
%% commented. If the value is `no', changes will be made
%% without comments. The default value is `yes'.</dd>
%%
%% <dt>`{redirect, [{atom(), atom()}]}'</dt>
%%
%% <dd>Specifies a list of pairs of module names, representing a
%% mapping from old names to new. <em>The set of old names may not
%% include any of the names of the input modules.</em> All calls to
%% the listed old modules will be rewritten to refer to the
%% corresponding new modules. <em>The redirected calls will not be
%% further processed, even if the new destination is in one of the
%% input modules.</em> This option mainly exists to support module
%% renaming; cf. `rename/3'. The default value is the
%% empty list.</dd>
%%
%% <dt>`{safe, [atom()]}'</dt>
%%
%% <dd>Specifies a list of names of input modules such that calls to
%% these "safe" modules may be turned into direct local calls, that
%% do not test for code replacement. Typically, this can be done for
%% e.g. standard library modules. If a module is "safe", it is per
%% definition also "static" (cf. below). The list may be empty. By
%% default, all involved modules <em>except the target module</em>
%% are considered "safe".</dd>
%%
%% <dt>`{static, [atom()]}'</dt>
%%
%% <dd>Specifies a list of names of input modules which will be
%% assumed never to be replaced (reloaded) unless the target module
%% is also first replaced. The list may be empty. The target module
%% itself (which may also be one of the input modules) is always
%% regarded as "static", regardless of the value of this option. By
%% default, all involved modules are assumed to be static.</dd>
%%
%% <dt>`{tidy, boolean()}'</dt>
%%
%% <dd>If the value is `true', the resulting code will be
%% processed using the `erl_tidy' module, which removes
%% unused functions and does general code cleanup. (See
%% `erl_tidy:module/2' for additional options.) The
%% default value is `true'.</dd>
%%
%% <dt>`{verbose, boolean()}'</dt>
%%
%% <dd>If the value is `true', progress messages will be
%% output while the program is running; the default value is
%% `false'.</dd>
%% </dl>
%%
%% Note: The distinction between "static" and "safe" modules is
%% necessary in order not to break the semantics of dynamic code
%% replacement. A "static" source module will not be replaced unless the
%% target module also is. Now imagine a state machine implemented by
%% placing the code for each state in a separate module, and suppose
%% that we want to merge this into a single target module, marking all
%% source modules as static. At each point in the original code where a
%% call is made from one of the modules to another (i.e., the state
%% transitions), code replacement is expected to be detected. Then, if
%% we in the merged code do not check at these points if the
%% <em>target</em> module (the result of the merge) has been replaced,
%% we can not be sure in general that we will be able to do code
%% replacement of the merged state machine - it could run forever
%% without detecting the code change. Therefore, all such calls must
%% remain remote-calls (detecting code changes), but may call the target
%% module directly.
%%
%% If we are sure that this kind of situation cannot ensue, we may
%% specify the involved modules as "safe", and all calls between them
%% will become local. Note that if the target module itself is specified
%% as safe, "remote" calls to itself will be turned into local calls.
%% This would destroy the code replacement properties of e.g. a typical
%% server loop.
%%
%% @see create_stubs/2
%% @see rename/3
%% @see erl_tidy:module/2
%% Currently, there is no run-time support in Erlang for detecting
%% whether some module has been changed since the current module was
%% loaded. Therefore, if a source module is specified as non-static, not
%% much will be gained from merging: a call to a non-static module will
%% remain a remote call using the old module name, even when it is
%% performed from within the merged code. If that module is specified as
%% exported, the old name could then refer to an auto-generated stub,
%% redirecting the call back to the corresponding function in the target
%% module. This could possibly be useful in some cases, but efficiency
%% is not improved by such a transformation. If support for efficient
%% testing for module updates is added to Erlang in future versions,
%% code merging will be able to use local calls even for non-static
%% source modules, opening the way for compiler optimisations over the
%% module boundaries.
%% Data structure for merging environment.
-record(merge, {target :: atom(),
sources :: ordset(atom()),
export :: ordset(atom()),
static :: ordset(atom()),
safe :: ordset(atom()),
preserved :: boolean(),
no_headers :: boolean(),
notes :: notes(),
redirect :: dict(), % = dict(atom(), atom())
no_imports :: ordset(atom()),
options :: [option()]
}).
-spec merge_sources(atom(), [erl_syntax:forms()], [option()]) ->
{erl_syntax:syntaxTree(), [stubDescriptor()]}.
merge_sources(Name, Sources, Opts) ->
%% Prepare the options and the inputs.
Opts1 = Opts ++ [{export_all, false},
{file_attributes, no},
{no_imports, false},
{notes, yes},
tidy,
{verbose, false}],
Trees = case Sources of
[] ->
report_error("no sources to merge."),
exit(badarg);
_ ->
[if is_list(M) -> erl_syntax:form_list(M);
true -> M
end
|| M <- Sources]
end,
%% There must be at least one module to work with.
Modules = [get_module_info(T) || T <- Trees],
merge_sources_1(Name, Modules, Trees, Opts1).
%% Data structure for keeping state during transformation.
-record(state, {export :: set()}).
state__add_export(Name, Arity, S) ->
S#state{export = sets:add_element({Name, Arity},
S#state.export)}.
merge_sources_1(Name, Modules, Trees, Opts) ->
%% Get the (nonempty) list of source module names, in the given
%% order. Multiple occurrences of the same source module name are
%% not accepted.
Ns = [M#module.name || M <- Modules],
case duplicates(Ns) of
[] ->
ok;
Ns1 ->
report_error("same module names repeated in input: ~p.",
[Ns1]),
exit(error)
end,
Sources = ordsets:from_list(Ns),
All = ordsets:add_element(Name, Sources),
%% Initialise the merging environment from the given options.
%%
%% If the `export' option is the empty list, then if the target
%% module is the same as one of the sources, that module will be
%% exported; otherwise the first listed source module is exported.
%% This simplifies use in most cases, and guarantees that the
%% generated module has a well-defined interface. If `export_all' is
%% `true', we expand it here by including the set of source module
%% names.
Es = case proplists:append_values(export, Opts) of
[] ->
case ordsets:is_element(Name, Sources) of
true ->
[Name];
false ->
[hd(Ns)]
end;
Es1 when is_list(Es1) ->
ordsets:from_list(Es1)
end,
Export = case proplists:get_bool(export_all, Opts) of
false ->
Es;
true ->
ordsets:union(Sources, Es)
end,
check_module_names(Export, Sources, "declared as exported"),
verbose("modules exported from `~w': ~p.", [Name, Export], Opts),
%% The target module is always "static". (Particularly useful when
%% the target is the same as one of the source modules). It is
%% however not "safe" by default. If no modules are explicitly
%% specified as static, it is assumed that *all* are static.
Static0 = ordsets:from_list(proplists:append_values(static, Opts)),
Static = case proplists:is_defined(static, Opts) of
false ->
All;
true ->
ordsets:add_element(Name, Static0)
end,
check_module_names(Static, All, "declared 'static'"),
verbose("static modules: ~p.", [Static], Opts),
%% If no modules are explicitly specified as "safe", it is assumed
%% that *all* source modules are "safe" except the target module and
%% those explicitly specified as "static".
Safe = case proplists:is_defined(safe, Opts) of
false ->
ordsets:subtract(Sources,
ordsets:add_element(Name, Static0));
true ->
ordsets:from_list(
proplists:append_values(safe, Opts))
end,
check_module_names(Safe, All, "declared 'safe'"),
verbose("safe modules: ~p.", [Safe], Opts),
Preserved = (ordsets:is_element(Name, Sources)
andalso ordsets:is_element(Name, Export))
orelse proplists:get_bool(no_banner, Opts),
NoHeaders = proplists:get_bool(no_headers, Opts),
Notes = proplists:get_value(notes, Opts, always),
Rs = proplists:append_values(redirect, Opts),
Redirect = case is_atom_map(Rs) of
true ->
Ms = ordsets:from_list([M || {M, _} <- Rs]),
case ordsets:intersection(Sources, Ms) of
[] ->
ok;
Ms1 ->
report_error("cannot redirect calls to "
"modules in input set: ~p.",
[Ms1]),
exit(error)
end,
dict:from_list(Rs);
false ->
report_error("bad value for `redirect' option: "
"~P.",
[Rs, 10]),
exit(error)
end,
NoImports = case proplists:get_bool(no_imports, Opts) of
true ->
ordsets:from_list(Sources ++
dict:fetch_keys(Redirect));
false ->
ordsets:from_list(dict:fetch_keys(Redirect))
end,
Env = #merge{target = Name,
sources = Sources,
export = Export,
safe = Safe,
static = Static,
preserved = Preserved,
no_headers = NoHeaders,
notes = Notes,
redirect = Redirect,
no_imports = NoImports,
options = Opts},
merge_sources_2(Env, Modules, Trees, Opts).
is_atom_map([{A1, A2} | As]) when is_atom(A1), is_atom(A2) ->
is_atom_map(As);
is_atom_map([]) ->
true;
is_atom_map(_) ->
false.
check_module_names(Names, Sources, Txt) ->
case Names -- Sources of
[] ->
ok;
Xs ->
report_error("unknown modules ~s: ~p.", [Txt, Xs]),
exit(error)
end.
%% This function performs all the stages of the actual merge:
merge_sources_2(Env, Modules, Trees, Opts) ->
%% Compute the merged name space and the list of renamings.
{Names, Renaming} = merge_namespaces(Modules, Env),
%% Merge the source module descriptions, computing a structure
%% describing the resulting module, and a table of aliases which
%% must be expanded.
{Module, Expansions} = merge_info(Modules, Names, Renaming,
Env),
%% Merge the actual source code, also returning the "original
%% header" (for the first code section in the output).
St = #state{export = sets:new()},
{Tree, Header, St1} = merge_code(Trees, Modules, Expansions,
Renaming, Env, St),
%% Filter out unwanted program forms and add a preamble to the code,
%% making a complete module.
Tree1 = erl_syntax:form_list([make_preamble(Module, Header,
Env, St1),
filter_forms(Tree, Env)]),
%% Tidy the final syntax tree (removing unused functions) and return
%% it together with the list of stub descriptors.
{tidy(Tree1, Opts), make_stubs(Modules, Renaming, Env)}.
make_preamble(Module, Header, Env, St) ->
Name = Module#module.name,
Vars = Module#module.vars,
Extras = ordsets:from_list(sets:to_list(St#state.export)),
Exports = make_exports(Module#module.exports, Extras),
Imports = make_imports(Module#module.aliases),
Attributes = make_attributes(Module#module.attributes),
erl_syntax:form_list(module_header(Header, Name, Vars, Env)
++ Exports
++ Imports
++ Attributes).
%% If the target preserves one of the source modules, we do not generate
%% a new header, but use the original.
module_header(Forms, Name, Vars, Env) ->
case Env#merge.preserved of
true ->
update_header(Forms, Name, Vars);
false ->
[comment([?COMMENT_BAR,
"This module was formed by merging "
"the following modules:",
""]
++ [lists:flatten(io_lib:fwrite("\t\t`~w'",
[M]))
|| M <- Env#merge.sources]
++ ["",
timestamp(),
""]),
erl_syntax:attribute(erl_syntax:atom('module'),
[erl_syntax:atom(Name)])]
end.
update_header(Fs, Name, Vars) ->
[M | Fs1] = lists:reverse(Fs),
Ps = if Vars =:= none -> [];
true -> [erl_syntax:list([erl_syntax:variable(V)
|| V <- Vars])]
end,
M1 = rewrite(M, erl_syntax:attribute(erl_syntax:atom('module'),
[erl_syntax:atom(Name) | Ps])),
lists:reverse([M1 | Fs1]).
%% Some functions may have been noted as necessary to export (because of
%% how they are called) even though the user did not specify that the
%% modules in which these functions originated should be part of the
%% interface of the resulting module.
make_exports(Exports, Extras) ->
case ordsets:subtract(Extras, Exports) of
[] ->
[make_export(Exports)];
Es ->
[make_export(Exports),
comment(["** The following exports "
"are not official: **"]),
make_export(Es)]
end.
make_export(Names) ->
Es = [erl_syntax:arity_qualifier(erl_syntax:atom(F),
erl_syntax:integer(A))
|| {F, A} <- Names],
if Es =:= [] ->
comment(["** Nothing is officially exported "
"from this module! **"]);
true ->
erl_syntax:attribute(erl_syntax:atom('export'),
[erl_syntax:list(Es)])
end.
%% Any aliases that cannot be expressed using `import' (i.e. those not
%% on the form `{F, {M, F}}') are ignored.
make_imports(As) ->
%% First remove any auto-imports and "non-proper" imports from
%% the list.
As1 = [A || {F, {_M, F}} = A <- As, not is_auto_import(F)],
[make_import(M, Fs) || {M, Fs} <- group_imports(As1)].
make_import(Module, Names) ->
Is = [erl_syntax:arity_qualifier(erl_syntax:atom(F),
erl_syntax:integer(A))
|| {F, A} <- Names],
erl_syntax:attribute(erl_syntax:atom('import'),
[erl_syntax:atom(Module),
erl_syntax:list(Is)]).
%% Group aliases by module.
group_imports(Imports) ->
dict:to_list(
lists:foldl(
fun ({F, {M, F}}, D) ->
case dict:find(M, D) of
{ok, V} ->
V1 = ordsets:add_element(F, V),
dict:store(M, V1, D);
error ->
dict:store(M, [F], D)
end
end,
dict:new(), Imports)).
%% ---------------------------------------------------------------------
%% Making stub descriptors
%%
%% These are generated for all exported modules that are not the target
%% module.
make_stubs(Modules, Renaming, Env) ->
make_stubs_1(Modules, Renaming, Env).
make_stubs_1([M | Ms], Renaming, Env) ->
Name = M#module.name,
if Name =/= Env#merge.target ->
case ordsets:is_element(Name, Env#merge.export) of
true ->
[make_stub(M, Renaming(Name), Env)
| make_stubs_1(Ms, Renaming, Env)];
false ->
make_stubs_1(Ms, Renaming, Env)
end;
true ->
make_stubs_1(Ms, Renaming, Env)
end;
make_stubs_1([], _, _) ->
[].
make_stub(M, Map, Env) ->
Target = Env#merge.target,
Es = [{F, {Target, Map(F)}} || F <- M#module.exports],
{M#module.name, Es, M#module.attributes}.
%% ---------------------------------------------------------------------
%% Removing and/or out-commenting program forms. The returned form
%% sequence tree is not necessarily flat.
-record(filter, {records :: set(), file_attributes, attributes}).
filter_forms(Tree, Env) ->
Forms = erl_syntax:form_list_elements(
erl_syntax:flatten_form_list(Tree)),
erl_syntax:form_list(filter_forms_1(Forms, Env)).
filter_forms_1(Forms, Env) ->
{Fs, _} = filter_forms_2(Forms, Env),
lists:reverse(Fs).
filter_forms_2(Forms, Env) ->
FileAttrsOpt = proplists:get_value(file_attributes,
Env#merge.options, comment),
%% Sanity check and translation of option value:
FileAttrs = case FileAttrsOpt of
yes -> keep;
no -> delete;
comment -> kill;
_ ->
report_error("invalid value for option "
"`file_attributes': ~w.",
[FileAttrsOpt]),
exit(error)
end,
Attrs = if length(Env#merge.sources) =:= 1 ->
delete; %% keeping the originals looks weird
true ->
kill
end,
S = #filter{records = sets:new(),
file_attributes = FileAttrs,
attributes = Attrs},
lists:foldl(
fun (F, {Fs, S0}) ->
case filter_form(F, S0) of
{keep, S1} ->
{[F | Fs], S1}; % keep
{kill, S1} ->
{[kill_form(F) | Fs], S1}; % kill
{delete, S1} ->
%% Remove, or kill if it has comments (only
%% top-level comments are examined).
case erl_syntax:has_comments(F) of
false ->
{Fs, S1};
true ->
{[kill_form(F) | Fs], S1}
end
end
end,
{[], S}, Forms).
filter_form(F, S) ->
case erl_syntax_lib:analyze_form(F) of
{attribute, {'file', _}} ->
{S#filter.file_attributes, S};
{attribute, {'module', _}} ->
{delete, S};
{attribute, {'export', _}} ->
{delete, S};
{attribute, {'import', _}} ->
{delete, S};
{attribute, {'record', {R, _}}} ->
Records = S#filter.records,
case sets:is_element(R, Records) of
true ->
{kill, S}; % already defined above
false ->
S1 = S#filter{records =
sets:add_element(R, Records)},
{keep, S1}
end;
{attribute, preprocessor} ->
{keep, S}; %% keep all preprocessor attributes
{attribute, _} ->
{S#filter.attributes, S}; %% handle all other attributes
{error_marker, _} ->
{delete, S};
{warning_marker, _} ->
{delete, S};
eof_marker ->
{delete, S}; % these must be deleted!
_ ->
{keep, S} % keep all other Erlang forms
end.
%% This out-comments (kills) a program form. Any top-level pre-comments
%% are moved out, to avoid "nested" comments.
kill_form(F) ->
F1 = erl_syntax:set_precomments(F, []),
F2 = erl_syntax_lib:to_comment(F1, ?KILL_PREFIX),
erl_syntax:set_precomments(F2, erl_syntax:get_precomments(F)).
%% ---------------------------------------------------------------------
%% Merging the name spaces of a set of modules. Returns the final set
%% (see module `sets') of names and a total renaming function (atom())
%% -> ({atom(), integer()}) -> {atom(), integer()}.
%%
%% Names are added in two passes, in order to avoid renaming the
%% interface functions whenever possible: all exported functions are
%% added to the name space before any nonexported are added, and
%% "exported" modules are taken before any other. Thus, the order is:
%%
%% - exported functions of exported modules
%% - exported functions of nonexported modules
%% - internal functions of exported modules
%% - internal functions of nonexported modules
%%
%% In fact, only the first group is important, but there might be some
%% point in establishing the above order, for better readability of the
%% final code.
merge_namespaces(Modules, Env) ->
Export = Env#merge.export,
Split = fun (M) ->
ordsets:is_element(M#module.name, Export)
end,
{M1, M2} = split_list(Split, Modules),
R = dict:new(),
Acc = {sets:new(), R},
{M3, Acc1} = merge_namespaces_1(M1, Acc),
%% Detect and warn about renamed interface functions
{_, Maps0} = Acc1,
case [{M, dict:to_list(Map)}
|| {M, Map} <- dict:to_list(Maps0), dict:size(Map) =/= 0] of
[] ->
ok;
Fs ->
report_warning("interface functions renamed:\n\t~p.", [Fs])
end,
{M4, Acc2} = merge_namespaces_1(M2, Acc1),
Ms = M3 ++ M4,
Acc3 = merge_namespaces_2(Ms, Acc2),
{{Names, Maps}, _} = merge_namespaces_3(Ms, Acc3),
{Names, make_renaming_function(Maps)}.
%% Adding exported names. (Note that the list gets a new temporary
%% format also containing the exports.) This first step initialises the
%% Maps "dict-of-dicts" structure.
merge_namespaces_1(Modules, Acc) ->
lists:mapfoldl(
fun (Module, {Names, Maps}) ->
Exports = sets:from_list(Module#module.exports),
M = Module#module.name,
{Names1, Map} = add_function_renamings(M, Exports, Names,
dict:new()),
Maps1 = dict:store(M, Map, Maps),
{{Module, Exports}, {Names1, Maps1}}
end,
Acc, Modules).
%% Adding nonexported names.
merge_namespaces_2(Modules, Acc) ->
lists:foldl(
fun ({Module, Exports}, {Names, Maps}) ->
Other = sets:subtract(
sets:from_list(Module#module.functions),
Exports),
M = Module#module.name,
Map = dict:fetch(M, Maps),
{Names1, Map1} = add_function_renamings(M, Other, Names,
Map),
Maps1 = dict:store(M, Map1, Maps),
{Names1, Maps1}
end,
Acc, Modules).
%% Adding record names. We need to keep a global
%% "record-definition-to-new-record-name" mapping RMap while doing this.
merge_namespaces_3(Modules, Acc) ->
lists:foldl(
fun ({Module, _Exports}, {{Names, Maps}, RMap}) ->
Records = Module#module.records,
M = Module#module.name,
Map = dict:fetch(M, Maps),
{Names1, Map1, RMap1} = add_record_renamings(M, Records,
Names, Map,
RMap),
Maps1 = dict:store(M, Map1, Maps),
{{Names1, Maps1}, RMap1}
end,
{Acc, dict:new()}, Modules).
%% This takes the set of added function names together with the existing
%% name set, creates new function names where necessary, and returns the
%% final name set together with the list of renamings.
add_function_renamings(Module, New, Names, Map) ->
Clashes = sets:to_list(sets:intersection(New, Names)),
lists:foldl(
fun (F = {_, A}, {Names, Map}) when is_integer(A) ->
F1 = new_function_name(Module, F, Names),
{sets:add_element(F1, Names), dict:store(F, F1, Map)}
end,
{sets:union(New, Names), Map}, Clashes).
%% This is similar to the above, but for record names. Note that we add
%% both the record name and the whole definition to the namespace.
add_record_renamings(Module, Records, Names, Map, RMap) ->
lists:foldl(
fun (N = {R, Fs}, {Names, Map, RMap}) ->
case sets:is_element(?record_name(R), Names) of
true ->
%% The name is already in use.
case sets:is_element(?record_name(N), Names) of
true ->
%% We have seen this definition before;
%% make sure we use the same name.
{R1, _} = remap_record_name(N, RMap),
Map1 = dict:store(?record_name(R),
?record_name(R1), Map),
{Names, Map1, RMap};
false ->
%% Redefinition of existing name. Create
%% new name and set up renamings.
N1 = {R1, _} = new_record_name(Module, R,
Fs, Names),
Map1 = dict:store(?record_name(R),
?record_name(R1), Map),
RMap1 = dict:store(N, N1, RMap),
Names1 = sets:add_element(?record_name(N1),
Names),
{Names1, Map1, RMap1}
end;
false ->
%% A previously unused record name.
Names1 = sets:add_element(?record_name(R), Names),
Names2 = sets:add_element(?record_name(N), Names1),
{Names2, Map, RMap}
end
end,
{Names, Map, RMap}, Records).
remap_record_name(N, Map) ->
case dict:find(N, Map) of
{ok, N1} -> N1;
error -> N
end.
%% This hides the implementation of the record namespace. Since Map
%% yields identity for non-remapped names, the remapped names must be
%% stored in wrapped form.
map_record_name(R, Map) ->
?record_name(R1) = Map(?record_name(R)),
R1.
%% When we rename a function, we want the new name to be as close as
%% possible to the old, and as informative as possible. Therefore, we
%% first prefix it with the name of the originating module, followed by
%% two underscore characters, and then if there still is a name clash,
%% we suffix the name by "_N", where N is the smallest possible positive
%% integer that does not cause a clash.
new_function_name(M, {F, A}, Names) ->
Base = atom_to_list(M) ++ "__" ++ atom_to_list(F),
Name = {list_to_atom(Base), A},
case sets:is_element(Name, Names) of
false ->
Name;
true ->
new_function_name(1, A, Base, Names)
end.
new_function_name(N, Arity, Base, Names) ->
Name = {list_to_atom(Base ++ "_" ++ integer_to_list(N)),
Arity},
case sets:is_element(Name, Names) of
false ->
Name;
true ->
%% Increment counter and try again.
new_function_name(N + 1, Arity, Base, Names)
end.
%% This is pretty much the same as new_function_name, for now.
new_record_name(M, R, Fs, Names) ->
Base = atom_to_list(M) ++ "__" ++ atom_to_list(R),
Name = {list_to_atom(Base), Fs},
case sets:is_element(?record_name(Name), Names) of
false ->
Name;
true ->
new_record_name_1(1, Base, Fs, Names)
end.
new_record_name_1(N, Base, Fs, Names) ->
Name = {list_to_atom(Base ++ "_" ++ integer_to_list(N)), Fs},
case sets:is_element(?record_name(Name), Names) of
false ->
Name;
true ->
%% Increment counter and try again.
new_record_name_1(N + 1, Base, Fs, Names)
end.
%% This returns a *total* function from the set of module names to the
%% set of *total* operators on function names, yielding identity for all
%% function names that are not specified in the given partial map
%% (ModuleName -> (Name -> Name)).
make_renaming_function(Maps) ->
fun (Module) ->
case dict:find(Module, Maps) of
{ok, Map} ->
fun (Name) ->
case dict:find(Name, Map) of
{ok, Name1} ->
Name1; % renamed
error ->
Name % identity
end
end;
error ->
%% Other module - yield identity map.
fun (Name) -> Name end
end
end.
%% ---------------------------------------------------------------------
%% Merging module info records into a target module record, and finding
%% necessary alias expansions. Returns `{Module, Expansions}' where
%% `Expansions' has type `dict(ModuleName, dict(Alias, FullName))'
merge_info(Modules, Names, Renaming, Env) ->
Forbid = sets:from_list(Env#merge.no_imports),
Expansions = alias_expansions(Modules, Names, Forbid),
Module = merge_info_1(Modules, Renaming, Expansions, Env),
{Module, Expansions}.
merge_info_1(Modules, Renaming, Expansions, Env) ->
lists:foldl(
fun (M, A) ->
Name = M#module.name,
Map = Renaming(Name),
Functions = join_functions(Map,
M#module.functions,
A#module.functions),
Exports = join_exports(Env, Name, Map,
M#module.exports,
A#module.exports),
Aliases = join_aliases(Name, Expansions,
M#module.aliases,
A#module.aliases),
Attributes = join_attributes(Env, Name,
M#module.attributes,
A#module.attributes),
Records = join_records(Map,
M#module.records,
A#module.records),
A#module{functions = Functions,
exports = Exports,
aliases = Aliases,
attributes = Attributes,
records = Records}
end,
#module{name = Env#merge.target,
functions = ordsets:new(),
exports = ordsets:new(),
aliases = ordsets:new(),
attributes = ordsets:new(),
records = ordsets:new()},
Modules).
%% Functions must be renamed before including.
join_functions(Map, Source, Target) ->
ordsets:union(ordsets:from_list([Map(A) || A <- Source]),
Target).
%% Exports also need renaming, and are kept only if their originating
%% modules are exported.
join_exports(Env, Name, Map, Source, Target) ->
case ordsets:is_element(Name, Env#merge.export) of
true ->
ordsets:union(ordsets:from_list([Map(F)
|| F <- Source]),
Target);
false ->
Target
end.
%% Aliases never need renaming; instead we always expand uses which
%% could cause name clashes. We must then remove the expanded names from
%% the imports of the target.
join_aliases(Name, Expansions, Source, Target) ->
As = case dict:find(Name, Expansions) of
{ok, As1} ->
ordsets:from_list(dict:to_list(As1));
error ->
[]
end,
ordsets:union(ordsets:subtract(Source, As), Target).
%% We only propagate attributes if the number of source modules is 1 or
%% the source module has the same name as the resulting module.
join_attributes(Env, Name, Source, Target) ->
if Env#merge.target =:= Name ->
ordsets:union(Source, Target);
true ->
if length(Env#merge.sources) =:= 1 ->
ordsets:union(Source, Target);
true ->
Target
end
end.
%% The final record info in itself is not used at present, but we
%% compute the join anyway. We apply renaming to records much like we do
%% to functions, but records have a separate namespace.
join_records(Map, Source, Target) ->
Renamed = [{map_record_name(R, Map), Fs} || {R, Fs} <- Source],
ordsets:union(ordsets:from_list(Renamed), Target).
%% This finds aliases that are in conflict or are for other reasons
%% necessary to expand while transforming the code later. It is assumed
%% that each module is in itself correct, and thus does not contain
%% conflicting definitions of the same alias.
%%
%% We could of course simply say that *all* aliases, without exception,
%% should be expanded, but such a big change in the style of the code
%% should not be done unless the user explicitly specifies it.
%%
%% The returned `Expansions' is a dictionary (module `dict') mapping
%% each module name in `Modules' to a dictionary which maps those
%% aliases to be expanded for that module to their corresponding full
%% names.
%%
%% Aliases are filtered according to the following rules:
%%
%% 1. If a name is defined (in some source module) as an alias of a
%% name `M:...', where `M' is any of the source modules(*), then
%% the definition of that alias should be removed, and all its uses
%% (in the same module as the definition) be expanded.
%%
%% 2. Then, if a name is defined (in some source module) as an
%% alias, but the name occurs in the name space of the resulting
%% module, then the definition should be removed and all uses (in
%% the same module) expanded.
%%
%% 3. Finally, if a name has two or more distinct alias definitions
%% in the source modules, then all definitions of that alias should
%% be removed and all uses (in all modules) expanded. (We remove
%% all definitions mainly for symmetry.)
%%
%% (*) It is actually possible for an alias to refer to the module
%% in which it is itself defined. However, since we also in this
%% case want to expand all uses, we don't have to do any extra work
%% to handle it.
%% The filtering is done in two stages.
alias_expansions(Modules, Names, Forbid) ->
Table = alias_expansions_1(Modules, Forbid, Names),
alias_expansions_2(Modules, Table).
%% First consider each alias in isolation.
alias_expansions_1(Modules, Forbid, Names) ->
lists:foldl(
fun (M, T) ->
Map = lists:foldl(
fun ({A, F}, T1) ->
case keep_alias(A, F, Forbid, Names)
of
true ->
T1;
false ->
dict:store(A, F, T1)
end
end,
dict:new(), M#module.aliases),
dict:store(M#module.name, Map, T)
end,
dict:new(), Modules).
keep_alias(A, {M, _}, Forbid, Names) ->
case sets:is_element(M, Forbid) of
true ->
false;
false ->
not sets:is_element(A, Names)
end.
%% In this second stage, we resolve any conflicts that may remain
%% because of distinct source modules still containing distinct alias
%% definitions of the same name - in that case we remove *all* of them
%% (mainly for symmetry).
alias_expansions_2(Modules, Table) ->
%% Get the set of all alias definitions in all modules (collapsing
%% duplicated but equivalent definitions).
Aliases = lists:foldl(
fun (M, A) ->
ordsets:union(A, M#module.aliases)
end,
ordsets:new(), Modules),
%% Get the set of names with multiple (distinct) definitions.
Names = duplicates([F || {F, _} <- Aliases]),
%% Go through all aliases in all source modules and add necessary
%% entries to the expansion-table. We expect that there is an entry
%% in the table here for each module.
lists:foldl(
fun (M, T) ->
N = M#module.name,
lists:foldl(
fun ({A, F}, T1) ->
case ordsets:is_element(A, Names) of
true ->
T2 = dict:fetch(N, T1),
dict:store(N,
dict:store(A, F, T2),
T1);
false ->
T1
end
end,
T, M#module.aliases)
end,
Table, Modules).
%% ---------------------------------------------------------------------
%% Merging the source code.
%% Data structure for code transformation environment.
-record(code, {module :: atom(),
target :: atom(),
sources :: set(), % set(atom()),
static :: set(), % set(atom()),
safe :: set(), % set(atom()),
preserved :: boolean(),
no_headers :: boolean(),
notes :: notes(),
map, % = ({atom(), int()}) -> {atom(), int()}
renaming, % = (atom()) -> ({atom(), int()}) ->
% {atom(), int()}
expand :: dict(), % = dict({atom(), int()},
% {atom(), {atom(), int()}})
redirect :: dict() % = dict(atom(), atom())
}).
%% `Trees' must be a list of syntax trees of type `form_list'. The
%% result is a pair `{Result, Header}' where `Result' is a `form_list'
%% tree representing the merged code, and if the `preserved' flag is
%% set, `Header' is the list of forms up to and including the first
%% `-module(...)' declaration, but stripped of any `-file(...)'
%% attributes - otherwise `Header' is an empty list.
merge_code(Trees, Modules, Expansions, Renaming, Env, St) ->
Env1 = #code{target = Env#merge.target,
sources = sets:from_list(Env#merge.sources),
static = sets:from_list(Env#merge.static),
safe = sets:from_list(Env#merge.safe),
preserved = Env#merge.preserved,
no_headers = Env#merge.no_headers,
notes = Env#merge.notes,
redirect = Env#merge.redirect,
renaming = Renaming},
Code = order_code(Modules, Trees, Env1),
{Code1, Header} = case Env1#code.preserved of
true ->
take_header(Code);
false ->
{Code, erl_syntax:form_list([])}
end,
{Forms, St1} = merge_code_1(Code1, Expansions, Env1, St),
Tree = erl_syntax:form_list(Forms),
{Tree, Header, St1}.
merge_code_1(Code, Expansions, Env, St) ->
lists:foldr(
fun ({Module, T}, {Acc, St0}) ->
M = Module#module.name,
Expand = case dict:find(M, Expansions) of
{ok, Dict} -> Dict;
error -> dict:new()
end,
Env1 = Env#code{module = M,
map = (Env#code.renaming)(M),
expand = Expand},
{T1, St1} = transform(T, Env1, St0),
{[section_header(M, T1, Env1) | Acc], St1}
end,
{[], St}, Code).
%% Pair module info and source code, in the order we want, and flatten
%% the form lists. If the name of the target is the same as one of the
%% source modules, and the result should preserve the original module,
%% the code for that module should be first in the output.
order_code(Modules, Trees, Env) ->
order_code(Modules, Trees, {}, [], Env).
order_code([M | Ms], [T | Ts], First, Rest, Env) ->
T1 = erl_syntax:flatten_form_list(T),
case (M#module.name =:= Env#code.target) and
Env#code.preserved of
true ->
order_code(Ms, Ts, {M, T1}, Rest, Env);
false ->
order_code(Ms, Ts, First, [{M, T1} | Rest], Env)
end;
order_code([], [], First, Rest, _Env) ->
Rest1 = lists:reverse(Rest),
case First of
{} ->
Rest1;
M ->
[M | Rest1]
end.
%% Extracting the "original" header (the `-module(...)' declaration is
%% sure to exist).
take_header([{M, T} | Ms]) ->
Fs = erl_syntax:form_list_elements(T),
{Header, Fs1} = take_header_1(Fs, []),
T1 = erl_syntax:form_list(Fs1),
{[{M, T1} | Ms], Header}.
take_header_1([F | Fs], As) ->
case erl_syntax_lib:analyze_form(F) of
{'attribute', {'module', _}} ->
{lists:reverse([F | As]), Fs}; % done
{'attribute', {'file', _}} ->
take_header_1(Fs, As); % discard
_ ->
take_header_1(Fs, [F | As]) % keep
end.
section_header(Name, Tree, Env) ->
N = sets:size(Env#code.sources),
if N > 1, Name =/= Env#code.target, Env#code.notes =/= no,
Env#code.no_headers =/= true ->
Text = io_lib:fwrite("The following code stems "
"from module `~w'.", [Name]),
Header = comment([?COMMENT_BAR, "",
lists:flatten(Text), ""]),
erl_syntax:form_list([Header, Tree]);
true ->
Tree
end.
transform(Tree, Env, St) ->
case erl_syntax:type(Tree) of
application ->
transform_application(Tree, Env, St);
attribute ->
transform_attribute(Tree, Env, St);
function ->
transform_function(Tree, Env, St);
implicit_fun ->
transform_implicit_fun(Tree, Env, St);
rule ->
transform_rule(Tree, Env, St);
record_expr ->
transform_record(Tree, Env, St);
record_index_expr ->
transform_record(Tree, Env, St);
record_access ->
transform_record(Tree, Env, St);
_ ->
default_transform(Tree, Env, St)
end.
default_transform(Tree, Env, St) ->
case erl_syntax:subtrees(Tree) of
[] ->
{Tree, St};
Gs ->
{Gs1, St1} = transform_1(Gs, Env, St),
Tree1 = rewrite(Tree, erl_syntax:make_tree(
erl_syntax:type(Tree),
Gs1)),
{Tree1, St1}
end.
transform_1([G | Gs], Env, St) ->
{G1, St1} = transform_list(G, Env, St),
{Gs1, St2} = transform_1(Gs, Env, St1),
{[G1 | Gs1], St2};
transform_1([], _Env, St) ->
{[], St}.
transform_list([T | Ts], Env, St) ->
{T1, St1} = transform(T, Env, St),
{Ts1, St2} = transform_list(Ts, Env, St1),
{[T1 | Ts1], St2};
transform_list([], _Env, St) ->
{[], St}.
%% Renaming function definitions
transform_function(T, Env, St) ->
{T1, St1} = default_transform(T, Env, St),
F = erl_syntax_lib:analyze_function(T1),
{V, Text} = case (Env#code.map)(F) of
F ->
%% Not renamed
{none, []};
{Atom, _Arity} ->
%% Renamed
Cs = erl_syntax:function_clauses(T1),
N = rename_atom(
erl_syntax:function_name(T1),
Atom),
T2 = erl_syntax:function(N, Cs),
{{value, T2}, renaming_note(Atom)}
end,
{maybe_modified(V, T1, 2, Text, Env), St1}.
renaming_note(Name) ->
[lists:flatten(io_lib:fwrite("renamed function to `~w'",
[Name]))].
rename_atom(Node, Atom) ->
rewrite(Node, erl_syntax:atom(Atom)).
%% Renaming Mnemosyne rules (just like function definitions)
transform_rule(T, Env, St) ->
{T1, St1} = default_transform(T, Env, St),
F = erl_syntax_lib:analyze_rule(T1),
{V, Text} = case (Env#code.map)(F) of
F ->
%% Not renamed
{none, []};
{Atom, _Arity} ->
%% Renamed
Cs = erl_syntax:rule_clauses(T1),
N = rename_atom(
erl_syntax:rule_name(T1),
Atom),
T2 = rewrite(T1,
erl_syntax:rule(N, Cs)),
{{value, T2}, renaming_note(Atom)}
end,
{maybe_modified(V, T1, 2, Text, Env), St1}.
%% Renaming "implicit fun" expressions (done quietly).
transform_implicit_fun(T, Env, St) ->
{T1, St1} = default_transform(T, Env, St),
F = erl_syntax_lib:analyze_implicit_fun(T1),
{V, Text} = case (Env#code.map)(F) of
F ->
%% Not renamed
{none, []};
{Atom, Arity} ->
%% Renamed
N = rewrite(
erl_syntax:implicit_fun_name(T1),
erl_syntax:arity_qualifier(
erl_syntax:atom(Atom),
erl_syntax:integer(Arity))),
T2 = erl_syntax:implicit_fun(N),
{{value, T2}, ["function was renamed"]}
end,
{maybe_modified_quiet(V, T1, 2, Text, Env), St1}.
%% Transforming function applications
transform_application(T, Env, St) ->
%% We transform the arguments first, so we can concentrate on the
%% application itself after that point.
{As, St1} = transform_list(
erl_syntax:application_arguments(T),
Env, St),
F = erl_syntax:application_operator(T),
%% See if the operator is an explicit function name.
%% (Usually, this will be the case.)
case catch {ok, erl_syntax_lib:analyze_function_name(F)} of
{ok, Name} ->
transform_application_1(Name, F, As, T, Env, St1);
syntax_error ->
%% Oper is not a function name, but might be any other
%% expression - we just visit it and reassemble the
%% application.
%% We do not handle applications of tuples `{M, F}'.
{F1, St2} = transform(F, Env, St1),
{rewrite(T, erl_syntax:application(F1, As)), St2};
{'EXIT', R} ->
exit(R);
R ->
throw(R)
end.
%% At this point we should have an explicit function name, which might
%% or might not be qualified by a module name.
transform_application_1(Name, F, As, T, Env, St) ->
%% Before doing anything else, we must unfold any uses of aliases
%% whose definitions have been killed.
Arity = length(As),
{Name1, F1} = expand_operator(Name, Arity, F, Env),
F2 = maybe_modified_quiet(F1, F, 7, ["unfolded alias"], Env),
{V, St1} = transform_application_2(Name1, Arity, F2, As, Env,
St),
T1 = rewrite(T, erl_syntax:application(F2, As)),
T3 = case V of
none ->
T1;
{value, {T2, Depth, Message}} ->
maybe_modified_quiet({value, T2}, T1, Depth,
Message, Env)
end,
{T3, St1}.
%% Here, Name has been expanded if necessary (if so, this is also
%% reflected by F), and As have been transformed. We should return
%% `{none, State}' if no further rewriting is necessary, and otherwise
%% `{{value, {Tree, Depth, Message}}, State}', where `Depth' and
%% `Message' are to be passed to `maybe_modified'.
transform_application_2(Name, Arity, F, As, Env, St)
when is_atom(Name) ->
transform_atom_application(Name, Arity, F, As, Env, St);
transform_application_2({M, N}, Arity, F, As, Env, St)
when is_atom(M), is_atom(N) ->
transform_qualified_application(M, N, Arity, F, As, Env, St);
transform_application_2(_Name, _Arity, _F, _As, _Env, St) ->
{none, St}. % strange name - do nothing.
expand_operator(Name, Arity, _F, Env) when is_atom(Name) ->
%% An unqualified function name - must be looked up. However, we
%% must first check that it is not an auto-imported name - these
%% have precedence over normal imports. We do a sanity check on the
%% found arity.
case is_auto_import({Name, Arity}) of
true ->
{Name, none}; % auto-import - never expand.
false ->
case dict:find({Name, Arity}, Env#code.expand) of
{ok, {M, {N, A}}} when A =:= Arity ->
%% Expand to a qualified name.
F1 = erl_syntax:module_qualifier(
erl_syntax:atom(M),
erl_syntax:atom(N)),
{{M, N}, {value, F1}};
error ->
%% Not in the table - leave it unchanged
{Name, none}
end
end;
expand_operator(Name, _Arity, _F, _Env) ->
%% Qualified function name - leave it unchanged
{Name, none}.
%% Transforming an application of a named function without module
%% qualifier (often misleadingly called "local" applications). Note that
%% since the `apply', `spawn' and `spawn_link' functions are implicitly
%% imported (from module `erlang'), applications of these names cannot
%% refer to functions defined in the source code.
transform_atom_application(Name, Arity, F, As, Env, St) ->
%% Catch applications of `apply' and `spawn'.
case {Name, Arity} of
{'apply', 2} ->
warning_apply_2(Env#code.module, Env#code.target),
{none, St};
{'apply', 3} ->
transform_apply_call(F, As, Env, St);
{'spawn', 3} ->
transform_spawn_call(F, As, Env, St);
{'spawn', 4} ->
transform_spawn_call(F, As, Env, St);
{'spawn_link', 3} ->
transform_spawn_call(F, As, Env, St);
{'spawn_link', 4} ->
transform_spawn_call(F, As, Env, St);
_ ->
%% A simple call of an unqualified function name - just
%% remap the name as necessary. Auto-imported names may not
%% be changed - the call never refers to a local function.
%% We do a sanity check on the arity.
case is_auto_import({Name, Arity}) of
true ->
{none, St}; % auto-import - do not change.
false ->
case (Env#code.map)({Name, Arity}) of
{N, A} when N =:= Name, A =:= Arity ->
%% Not changed.
{none, St};
{N, A} when A =:= Arity ->
%% The callee (in the current module)
%% was renamed.
F1 = rewrite(F, erl_syntax:atom(N)),
T = erl_syntax:application(F1, As),
V = {T, 2, ["callee was renamed"]},
{{value, V}, St}
end
end
end.
%% Transforming an application of an explicitly named function qualified
%% with an (also explicit) module name. (Often called "remote"
%% applications.)
transform_qualified_application(Module, Name, Arity, F, As, Env, St) ->
%% Catch applications of `apply' and `spawn'.
case {Module, Name, Arity} of
{'erlang', 'apply', 2} ->
warning_apply_2(Env#code.module, Env#code.target),
{none, St};
{'erlang', 'apply', 3} ->
transform_apply_call(F, As, Env, St);
{'erlang', 'spawn', 3} ->
transform_spawn_call(F, As, Env, St);
{'erlang', 'spawn', 4} ->
transform_spawn_call(F, As, Env, St);
{'erlang', 'spawn_link', 3} ->
transform_spawn_call(F, As, Env, St);
{'erlang', 'spawn_link', 4} ->
transform_spawn_call(F, As, Env, St);
_ ->
case erlang:is_builtin(Module, Name, Arity) of
false ->
transform_qualified_application_1(
Module, Name, Arity, F, As, Env, St);
true ->
{none, St}
end
end.
transform_qualified_application_1(Module, Name, Arity, F, As, Env,
St) ->
MakeLocal = fun (N) ->
F1 = rewrite(F, erl_syntax:atom(N)),
erl_syntax:application(F1, As)
end,
MakeRemote = fun () ->
erl_syntax:application(F, As)
end,
MakeDynamic = fun(M, N) ->
F1 = erl_syntax:module_qualifier(
erl_syntax:atom(M),
erl_syntax:atom(N)),
F2 = rewrite(F, F1),
erl_syntax:application(F2, As)
end,
localise(Module, Name, Arity, MakeLocal, MakeRemote,
MakeDynamic, 3, Env, St).
%% For an `apply/3' call, if we know the called module and function
%% names, and the number of arguments, then we can rewrite it to a
%% direct remote call - and if we do not, there is nothing we can
%% change.
transform_apply_call(F, As, Env, St) ->
[Module, Name, List] = As,
case (erl_syntax:type(Module) =:= atom)
and (erl_syntax:type(Name) =:= atom)
and erl_syntax:is_proper_list(List) of
true ->
transform_apply_call_1(Module, Name, List, F, As, Env,
St);
false ->
%% We can't get enough information about the
%% arguments to the `apply' call, so we do nothing
%% but warn.
warning_unsafe_call(apply, Env#code.module,
Env#code.target),
{none, St}
end.
%% Rewrite the apply-call to a static qualified call and handle that
%% instead.
transform_apply_call_1(Module, Name, List, F, _As, Env, St) ->
F1 = rewrite(F, erl_syntax:module_qualifier( Module, Name)),
As1 = erl_syntax:list_elements(List),
M = erl_syntax:atom_value(Module),
N = erl_syntax:atom_value(Name),
A = length(As1),
transform_qualified_application_1(M, N, A, F1, As1, Env, St).
%% `spawn' and `spawn_link' (with arity 3 or 4) are very much like
%% `apply/3', but there could be an extra `Node' argument. Note that `F'
%% below can represent both `spawn' and `spawn_link'.
transform_spawn_call(F, As, Env, St) ->
case As of
[Module, Name, List] ->
MakeSpawn = fun (As1) ->
erl_syntax:application(F, As1)
end,
transform_spawn_call_1(Module, Name, List, MakeSpawn,
Env, St);
[Node, Module, Name, List] ->
MakeSpawn = fun (As1) ->
erl_syntax:application(
F, [Node | As1])
end,
transform_spawn_call_1(Module, Name, List, MakeSpawn,
Env, St)
end.
%% Here, we can treat all dynamic-lookup spawns like `spawn/3'.
transform_spawn_call_1(Module, Name, List, MakeSpawn, Env, St) ->
case (erl_syntax:type(Module) =:= atom)
and (erl_syntax:type(Name) =:= atom)
and erl_syntax:is_proper_list(List)
of
true ->
transform_spawn_call_2(Module, Name, List, MakeSpawn,
Env, St);
_ ->
%% We can't get enough information about the arguments to
%% the `spawn' call, so we do nothing but warn.
warning_unsafe_call(spawn, Env#code.module,
Env#code.target),
{none, St}
end.
transform_spawn_call_2(Module, Name, List, MakeSpawn, Env, St) ->
As = erl_syntax:list_elements(List),
Arity = length(As),
MakeLocal = fun (N) ->
%% By using `spawn-a-fun', we do not have to
%% force the callee to be exported.
A = rewrite(Name, erl_syntax:atom(N)),
B = erl_syntax:application(A, As),
C = erl_syntax:clause([], [B]),
F = erl_syntax:fun_expr([C]),
MakeSpawn([F])
end,
MakeRemote = fun () ->
MakeSpawn([Module, Name, List])
end,
MakeDynamic = fun (M, N) ->
F = rewrite(Name, erl_syntax:atom(N)),
MakeSpawn([erl_syntax:atom(M), F, List])
end,
localise(erl_syntax:atom_value(Module),
erl_syntax:atom_value(Name),
Arity, MakeLocal, MakeRemote, MakeDynamic,
4, Env, St).
%% MakeLocal = (atom()) -> syntaxTree()
%% MakeRemote = () -> syntaxTree()
%% MakeDynamic = (atom(), atom()) -> syntaxTree()
%% localise(...) -> {none, state()} | {{value, V}, State}
localise(Module, Name, Arity, MakeLocal, MakeRemote, MakeDynamic,
Depth, Env, St) ->
%% Is the callee in one of the source modules?
case sets:is_element(Module, Env#code.sources) of
false ->
case dict:find(Module, Env#code.redirect) of
{ok, Module1} ->
T = MakeDynamic(Module1, Name),
V = {T, Depth, ["redirected call"]},
{{value, V}, St};
error ->
{none, St} % Nothing needs doing.
end;
true ->
%% Remap the name of the callee, as necessary. Do a sanity
%% check on the arity.
Map = (Env#code.renaming)(Module),
Name1 = case Map({Name, Arity}) of
{N, A} when A =:= Arity ->
N
end,
%% See if the callee module is "safe" and/or "static".
Safe = sets:is_element(Module, Env#code.safe),
Static = (sets:is_element(Module, Env#code.static)
or Safe),
%% Select what kind of code to generate for the call:
case Static of
false ->
%% (This also implies that the called module is not
%% the target module - which is always "static" -
%% and that it is not "safe".) The called module
%% could be replaced dynamically, independent of the
%% target module, so we must protect the localised
%% call. We strip all comments from the localised
%% code, to avoid getting the same comments twice.
L = MakeLocal(Name1),
L1 = erl_syntax_lib:strip_comments(L),
R = MakeRemote(),
{T, Text} = protect_call(Module, L1, R),
V = {T, Depth, Text},
{{value, V}, St};
true ->
%% In this case, the called module is never replaced
%% unless the target module also is. (N.B.: These
%% might be the same module.)
case Safe of
false ->
%% The normal code replacement semantics
%% must be preserved here, so the generated
%% call must be qualified with the name of
%% the target module. (We assume this is
%% efficiently compiled even if we do not
%% insert an explicit "latest version"
%% test.)
Target = Env#code.target,
case Module =:= Target of
true ->
%% Already calling the target module
%% - do not insert irritating notes.
{none, St};
false ->
%% We must ensure that the function
%% is exported.
St1 = state__add_export(Name1,
Arity, St),
T = MakeDynamic(Target, Name1),
Text = ["localised call"],
V = {T, Depth, Text},
{{value, V}, St1}
end;
true ->
%% The call is regarded as safe to localise
%% completely. Code replacement will in
%% general not be detected (except for
%% spawn/apply).
T = MakeLocal(Name1),
Text = ["localised safe call"],
V = {T, Depth, Text},
{{value, V}, St}
end
end
end.
%%% %% This creates a test on whether there is a later loaded version of
%%% %% Module: if not, select the `Local' expression, otherwise the `Remote'
%%% %% expression. We knowingly duplicate code here, to allow better
%%% %% optimisations, but we never duplicate work.
%%%
%%% protect_call(Module, Local, Remote) ->
%%% T = erl_syntax:if_expr(
%%% [erl_syntax:clause([erl_syntax:application(
%%% erl_syntax:atom('not_replaced'),
%%% [erl_syntax:atom(Module)])],
%%% [Local]),
%%% erl_syntax:clause([erl_syntax:atom('true')],
%%% [Remote])]),
%%% {T, ["localised dynamic call"]}.
%% This "protects" a localised call by letting it remain a remote call.
protect_call(_Module, _Local, Remote) ->
{Remote, ["dynamic call"]}.
%% Renaming record declarations
transform_attribute(T, Env, St) ->
{T1, St1} = TSt1 = default_transform(T, Env, St),
case erl_syntax_lib:analyze_attribute(T1) of
{record, {R, _}} ->
F = fun(R) ->
[_ | As] = erl_syntax:attribute_arguments(T1),
erl_syntax:attribute(
erl_syntax:attribute_name(T1),
[erl_syntax:atom(R) | As])
end,
{V, Text} = rename_record(R, F, Env),
{maybe_modified(V, T1, 2, Text, Env), St1};
_ ->
TSt1
end.
%% This handles renaming of records.
transform_record(T, Env, St) ->
{T1, St1} = TSt1 = default_transform(T, Env, St),
X = case catch erl_syntax_lib:analyze_record_expr(T1) of
{record_expr, {R, _}} ->
F = fun (R) ->
erl_syntax:record_expr(
erl_syntax:record_expr_argument(T1),
erl_syntax:atom(R),
erl_syntax:record_expr_fields(T1))
end,
{R, F};
{record_index_expr, {R, _}} ->
F = fun (R) ->
erl_syntax:record_index_expr(
erl_syntax:atom(R),
erl_syntax:record_index_expr_field(T1))
end,
{R, F};
{record_access, {R, _}} ->
F = fun (R) ->
erl_syntax:record_access(
erl_syntax:record_access_argument(T1),
erl_syntax:atom(R),
erl_syntax:record_access_field(T1))
end,
{R, F};
_Type ->
false
end,
case X of
{R1, F1} ->
{V, Text} = rename_record(R1, F1, Env),
{maybe_modified(V, T1, 1, Text, Env), St1};
false ->
TSt1
end.
rename_record(R, F, Env) ->
case map_record_name(R, Env#code.map) of
R ->
%% Not renamed
{none, []};
R1 ->
%% Renamed
{{value, F(R1)}, ["record was renamed"]}
end.
%% Maybe-rewriting Node, adding modification notes.
%% This is for non-primary modifications; they are not commented unless
%% the `notes' option is set to `always'.
maybe_modified_quiet(V, Node, Depth, Message, Env) ->
case Env#code.notes of
always ->
maybe_modified_1(V, Node, Depth, Message, yes);
_ ->
maybe_modified_1(V, Node, Depth, Message, no)
end.
%% This is for important notes; they are only disabled if the `notes'
%% option is set to `no'.
maybe_modified(V, Node, Depth, Message, Env) ->
maybe_modified_1(V, Node, Depth, Message, Env#code.notes).
maybe_modified_1(none, Node, _Depth, _Message, _Notes) ->
Node;
maybe_modified_1({value, Node1}, Node, Depth, Message, Notes) ->
case Notes of
no ->
rewrite(Node, Node1);
_ ->
Code = erl_syntax:comment_text(
erl_syntax_lib:to_comment(
erl_syntax_lib:strip_comments(
erl_syntax_lib:limit(Node, Depth)),
"\040\040")),
erl_syntax:add_precomments(
[comment_note(Message ++
["Original code:" | Code])],
rewrite(Node, Node1))
end.
%% =====================================================================
%% @spec create_stubs(Stubs::[stubDescriptor()], Options::[term()]) ->
%% [string()]
%%
%% @doc Creates stub module source files corresponding to the given stub
%% descriptors. The returned value is the list of names of the created
%% files. See `merge_sources/3' for more information about
%% stub descriptors.
%%
%% Options:
%% <dl>
%% <dt>`{backup_suffix, string()}'</dt>
%% <dt>`{backups, boolean()}'</dt>
%% <dt>`{printer, Function}'</dt>
%% <dt>`{stub_dir, filename()}'</dt>
%% <dt>`{suffix, string()}'</dt>
%% <dt>`{verbose, boolean()}'</dt>
%% </dl>
%%
%% See `merge/3' for details on these options.
%%
%% @see merge/3
%% @see merge_sources/3
-spec create_stubs([stubDescriptor()], [option()]) -> [string()].
create_stubs(Stubs, Opts) ->
Opts1 = Opts ++ ?DEFAULT_MERGE_OPTS,
lists:foldl(fun (S, Fs) ->
F = create_stub(S, Opts1),
[F | Fs]
end,
[], Stubs).
maybe_create_stubs(Stubs, Opts) ->
case proplists:get_bool(stubs, Opts) of
true ->
create_stubs(Stubs, Opts);
false ->
[]
end.
create_stub({Name, Fs, Attrs}, Opts) ->
Defs = [stub_function(F) || F <- Fs],
Exports = [F || {F, _} <- Fs],
Forms = stub_header(Name, Exports, Attrs) ++ Defs,
Dir = proplists:get_value(stub_dir, Opts, ""),
verbose("creating stub file for module `~w'.", [Name], Opts),
write_module(erl_syntax:form_list(Forms), Name, Dir, Opts).
%% We just follow the arity specifications naively when we create the
%% stub funcion - it is not our responsibility to check them.
stub_function({{F, A}, {M, {F1, A1}}}) ->
Vs = var_list(A),
Vs1 = var_list(A1),
R = erl_syntax:module_qualifier(erl_syntax:atom(M),
erl_syntax:atom(F1)),
Call = erl_syntax:application(R, Vs1),
erl_syntax:function(erl_syntax:atom(F),
[erl_syntax:clause(Vs, [], [Call])]).
var_list(N) ->
var_list(N, 1).
var_list(N, I) when N > 0 ->
[erl_syntax:variable("X" ++ integer_to_list(I))
| var_list(N - 1, I + 1)];
var_list(0, _) ->
[].
stub_header(Name, Exports, Attrs) ->
[comment([?COMMENT_BAR,
io_lib:fwrite("This is an automatically "
"generated stub interface\n"
"for the module `~w'.",
[Name]),
"",
timestamp(),
""]),
erl_syntax:attribute(erl_syntax:atom('module'),
[erl_syntax:atom(Name)]),
make_export(Exports)]
++ make_attributes(Attrs).
%% =====================================================================
-type renamings() :: [{atom(), atom()}].
%% =====================================================================
%% @spec rename(Files::[filename()], Renamings) -> [string()]
%% @equiv rename(Files, Renamings, [])
-spec rename([file:filename()], renamings()) -> [string()].
rename(Files, Renamings) ->
rename(Files, Renamings, []).
%% =====================================================================
%% @spec rename(Files::[filename()], Renamings, Options::[term()]) ->
%% [string()]
%%
%% Renamings = [{atom(), atom()}]
%%
%% @doc Renames a set of possibly interdependent source code modules.
%% `Files' is a list of file names of source modules to be
%% processed. `Renamings' is a list of pairs of <em>module
%% names</em>, representing a mapping from old names to new. The
%% returned value is the list of output file names.
%%
%% Each file in the list will be read and processed separately. For
%% every file, each reference to some module M, such that there is an
%% entry `{<em>M</em>, <em>M1</em>}' in
%% `Renamings', will be changed to the corresponding M1.
%% Furthermore, if a file F defines module M, and there is an entry
%% `{<em>M</em>, <em>M1</em>}' in `Renamings', a
%% new file named `<em>M1</em>.erl' will be created in the
%% same directory as F, containing the source code for module M, renamed
%% to M1. If M does not have an entry in `Renamings', the
%% module is not renamed, only updated, and the resulting source code is
%% written to `<em>M</em>.erl' (typically, this overwrites
%% the original file). The `suffix' option (see below) can be
%% used to change the default "`.erl'" suffix for the
%% generated files.
%%
%% Stub modules will automatically be created (see the
%% `stubs' and `stub_dir' options below) for each
%% module that is renamed. These can be used to redirect any calls still
%% using the old module names. The stub files are created in the same
%% directory as the source file (typically overwriting the original
%% file).
%%
%% Options:
%% <dl>
%% <dt>`{backup_suffix, string()}'</dt>
%% <dt>`{backups, boolean()}'</dt>
%% <dt>`{printer, Function}'</dt>
%% <dt>`{stubs, boolean()}'</dt>
%% <dt>`{suffix, string()}'</dt>
%% </dl>
%% See `merge/3' for details on these options.
%%
%% <dl>
%% <dt>`{comments, boolean()}'</dt>
%% <dt>`{preprocess, boolean()}'</dt>
%% </dl>
%% See `merge_files/4' for details on these options.
%%
%% <dl>
%% <dt>`{no_banner, boolean()}'</dt>
%% </dl>
%% For the `rename' function, this option is
%% `true' by default. See `merge_sources/3' for
%% details.
%%
%% <dl>
%% <dt>`{tidy, boolean()}'</dt>
%% </dl>
%% For the `rename' function, this option is
%% `false' by default. See `merge_sources/3' for
%% details.
%%
%% <dl>
%% <dt>`{no_headers, boolean()}'</dt>
%% <dt>`{stub_dir, filename()}'</dt>
%% </dl>
%% These options are preset by the `rename' function and
%% cannot be overridden by the user.
%%
%% See `merge_sources/3' for further options.
%%
%% @see merge/3
%% @see merge_sources/3
%% @see merge_files/4
-spec rename([file:filename()], renamings(), [term()]) -> [string()].
rename(Files, Renamings, Opts) ->
Dict = case is_atom_map(Renamings) of
true ->
dict:from_list(Renamings);
false ->
report_error("bad module renaming: ~P.",
[Renamings, 10]),
exit(error)
end,
%% We disable *all* automatic source code lookup, for safety: you
%% are only allowed to do renaming on a module if you give its path.
Opts1 = [{find_src_rules, []}]
++ Opts ++ [{backup_suffix, ?DEFAULT_BACKUP_SUFFIX},
backups,
{printer, fun default_printer/2},
stubs,
{suffix, ?DEFAULT_SUFFIX},
comments,
{preprocess, false},
{tidy, false},
no_banner,
{notes, no},
{verbose, false}],
lists:flatmap(fun (F) -> rename_file(F, Dict, Opts1) end, Files).
rename_file(File, Dict, Opts) ->
S = read_module(File, Opts),
M = get_module_info(S),
Name = M#module.name,
Name1 = case dict:find(Name, Dict) of
{ok, N} -> N;
error -> Name
end,
%% We convert the dictionary to a new list to ensure that we use the
%% exact same renaming for redirections. We must remove the current
%% module from the redirection set.
Dict1 = dict:erase(Name, Dict),
Opts1 = [no_headers,
{export, [Name]},
{static, [Name]},
{redirect, dict:to_list(Dict1)}] ++ Opts,
{Tree, Stubs} = merge_sources(Name1, [S], Opts1),
Dir = filename:dirname(filename(File)),
File1 = write_module(Tree, Name1, Dir, Opts),
%% We create the stub file in the same directory as the source file
%% and the target file.
[File1 | maybe_create_stubs(Stubs, [{stub_dir, Dir} | Opts1])].
%% ---------------------------------------------------------------------
%% Initialise a module-info record with data about the module
%% represented by the syntax tree (or list of "forms"). Listed exports
%% are guaranteed to be in the set of function names.
get_module_info(Forms) ->
L = case catch {ok, erl_syntax_lib:analyze_forms(Forms)} of
{ok, L1} ->
L1;
syntax_error ->
report_error("syntax error in input."),
erlang:error(badarg);
{'EXIT', R} ->
exit(R);
R ->
throw(R)
end,
{Name, Vars} =
case lists:keyfind(module, 1, L) of
{module, {_N, _Vs} = NVs} ->
NVs;
{module, N} ->
{N, none};
false ->
report_error("in source code: module name missing."),
exit(error)
end,
case lists:keyfind(errors, 1, L) of
{errors, Ds} when Ds =/= [] ->
report_errors(Ds, Name),
exit(error);
_ ->
ok
end,
case lists:keyfind(warnings, 1, L) of
{warnings, Ds1} when Ds1 =/= [] ->
report_warnings(Ds1, Name);
_ ->
ok
end,
Functions = case lists:keyfind(functions, 1, L) of
{functions, Fs} ->
ordsets:from_list(Fs);
_ ->
[]
end,
Exports = case lists:keyfind(exports, 1, L) of
{exports, Es} ->
ordsets:from_list(Es);
_ ->
[]
end,
Imports = case lists:keyfind(imports, 1, L) of
{imports, Is} ->
expand_imports(Is, Name);
_ ->
[]
end,
Attributes = case lists:keyfind(attributes, 1, L) of
{attributes, As} ->
ordsets:from_list(As);
_ ->
[]
end,
Records = case lists:keyfind(records, 1, L) of
{records, Rs} ->
fold_record_fields(Rs);
_ ->
[]
end,
check_records(Records, Name),
#module{name = Name,
vars = Vars,
functions = Functions,
exports = ordsets:intersection(Exports, Functions),
aliases = Imports,
attributes = Attributes,
records = Records}.
fold_record_fields(Rs) ->
[{N, [fold_record_field(F) || F <- Fs]} || {N, Fs} <- Rs].
fold_record_field({_Name, none} = None) ->
None;
fold_record_field({Name, F}) ->
case erl_syntax:is_literal(F) of
true ->
{Name, {value, erl_syntax:concrete(F)}};
false ->
%% The default value for the field is not a constant, so we
%% represent it by a hash value instead. (We don't want to
%% do this in the general case.)
{Name, {hash, erlang:phash(F, 16#ffffff)}}
end.
report_errors([D | Ds], Name) ->
report_error("error: " ++ error_text(D, Name)),
report_errors(Ds, Name);
report_errors([], _) ->
ok.
report_warnings([D | Ds], Name) ->
report_warning(error_text(D, Name)),
report_errors(Ds, Name);
report_warnings([], _) ->
ok.
error_text(D, Name) ->
case D of
{L, M, E} when is_integer(L), is_atom(M) ->
case catch M:format_error(E) of
S when is_list(S) ->
io_lib:fwrite("`~w', line ~w: ~s.",
[Name, L, S]);
_ ->
error_text_1(D, Name)
end;
_E ->
error_text_1(D, Name)
end.
error_text_1(D, Name) ->
io_lib:fwrite("error: `~w', ~P.", [Name, D, 15]).
check_records(Rs, Name) ->
case duplicates([N || {N, _} <- Rs]) of
[] ->
ok;
Ns ->
report_error("in module `~w': "
"multiply defined records: ~p.",
[Name, Ns]),
exit(error)
end.
expand_imports(Is, Name) ->
Fs = ordsets:from_list(lists:append([[{M, F} || F <- Fs]
|| {M, Fs} <- Is])),
As = erl_syntax_lib:function_name_expansions(Fs),
case duplicates([N || {N, _} <- As]) of
[] ->
ordsets:from_list(As);
Ns ->
report_error("in module `~w': "
"multiply imported functions: ~p.",
[Name, Ns]),
exit(error)
end.
%% ---------------------------------------------------------------------
%% File handling
%% open_output_file(filename()) -> filedescriptor()
open_output_file(FName) ->
case catch file:open(FName, [write]) of
{ok, FD} ->
FD;
{error, _} = Error ->
error_open_output(FName),
exit(Error);
{'EXIT', R} ->
error_open_output(FName),
exit(R);
R ->
error_open_output(FName),
exit(R)
end.
%% read_module(Name, Options) -> syntaxTree()
%%
%% This also tries to locate the real source file, if "Name" does not
%% point directly to a particular file.
read_module(Name, Options) ->
case file_type(Name) of
{value, _} ->
read_module_1(Name, Options);
none ->
Rules = proplists:get_value(find_src_rules, Options),
case find_src(Name, Rules) of
{error, _} ->
%% It seems that we have no file - go on anyway,
%% just to get a decent error message.
read_module_1(Name, Options);
{Name1, _} ->
read_module_1(Name1 ++ ".erl", Options)
end
end.
read_module_1(Name, Options) ->
verbose("reading module `~s'.", [filename(Name)], Options),
Forms = read_module_2(Name, Options),
case proplists:get_bool(comments, Options) of
false ->
Forms;
true ->
Comments = erl_comment_scan:file(Name),
erl_recomment:recomment_forms(Forms, Comments)
end.
read_module_2(Name, Options) ->
case read_module_3(Name, Options) of
{ok, Forms} ->
check_forms(Forms, Name),
Forms;
{error, _} = Error ->
error_read_file(Name),
exit(Error)
end.
read_module_3(Name, Options) ->
case proplists:get_bool(preprocess, Options) of
false ->
epp_dodger:parse_file(Name);
true ->
read_module_4(Name, Options)
end.
read_module_4(Name, Options) ->
Includes = proplists:append_values(includes, Options)
++ [filename:dirname(Name) | ?DEFAULT_INCLUDES],
Macros = proplists:append_values(macros, Options)
++ ?DEFAULT_MACROS,
epp:parse_file(Name, Includes, Macros).
check_forms([F | Fs], File) ->
case erl_syntax:type(F) of
error_marker ->
S = case erl_syntax:error_marker_info(F) of
{_, M, D} ->
M:format_error(D);
_ ->
"unknown error"
end,
report_error("in file `~s' at line ~w:\n ~s",
[filename(File), erl_syntax:get_pos(F), S]),
exit(error);
_ ->
check_forms(Fs, File)
end;
check_forms([], _) ->
ok.
find_src(Name, undefined) ->
filename:find_src(filename(Name));
find_src(Name, Rules) ->
filename:find_src(filename(Name), Rules).
%% file_type(filename()) -> {value, Type} | none
file_type(Name) ->
case catch file:read_file_info(Name) of
{ok, Env} ->
{value, Env#file_info.type};
{error, enoent} ->
none;
{error, _} = Error ->
error_read_file_info(Name),
exit(Error);
{'EXIT', R} ->
error_read_file_info(Name),
exit(R);
R ->
error_read_file_info(Name),
throw(R)
end.
%% Create the target directory and make a backup file if necessary, then
%% open the file, output the text and close the file safely. Returns the
%% file name.
write_module(Tree, Name, Dir, Opts) ->
Name1 = filename(Name),
Dir1 = filename(Dir),
Base = if Dir1 =:= "" ->
Name1;
true ->
case file_type(Dir1) of
{value, directory} ->
ok;
{value, _} ->
report_error("`~s' is not a directory.",
[Dir1]),
exit(error);
none ->
case file:make_dir(Dir1) of
ok ->
verbose("created directory `~s'.",
[Dir1], Opts),
ok;
E ->
report_error("failed to create "
"directory `~s'.",
[Dir1]),
exit({make_dir, E})
end
end,
filename:join(Dir1, Name1)
end,
Suffix = proplists:get_value(suffix, Opts, ""),
File = Base ++ Suffix,
case proplists:get_bool(backups, Opts) of
true ->
backup_file(File, Opts);
false ->
ok
end,
Printer = proplists:get_value(printer, Opts),
FD = open_output_file(File),
verbose("writing to file `~s'.", [File], Opts),
V = (catch {ok, output(FD, Printer, Tree, Opts)}),
ok = file:close(FD),
case V of
{ok, _} ->
File;
{'EXIT', R} ->
error_write_file(File),
exit(R);
R ->
error_write_file(File),
throw(R)
end.
output(FD, Printer, Tree, Opts) ->
io:put_chars(FD, Printer(Tree, Opts)),
io:nl(FD).
%% If the file exists, rename it by appending the given suffix to the
%% file name.
backup_file(Name, Opts) ->
case file_type(Name) of
{value, regular} ->
backup_file_1(Name, Opts);
{value, _} ->
error_backup_file(Name),
exit(error);
none ->
ok
end.
%% The file should exist and be a regular file here.
backup_file_1(Name, Opts) ->
Name1 = filename(Name),
Suffix = proplists:get_value(backup_suffix, Opts, ""),
Dest = filename:join(filename:dirname(Name1),
filename:basename(Name1) ++ Suffix),
case catch file:rename(Name1, Dest) of
ok ->
verbose("made backup of file `~s'.", [Name1], Opts);
{error, R} ->
error_backup_file(Name1),
exit({error, R});
{'EXIT', R} ->
error_backup_file(Name1),
exit(R);
R ->
error_backup_file(Name1),
throw(R)
end.
%% =====================================================================
%% Utility functions
%% The form sequence returned by 'erl_tidy:module' is flat, even if the
%% given tree is not.
tidy(Tree, Opts) ->
case proplists:get_bool(tidy, Opts) of
true ->
verbose("tidying final module.", Opts),
erl_tidy:module(Tree, ?TIDY_OPTS);
false ->
Tree
end.
make_attributes(As) ->
[make_attribute(A) || A <- As].
make_attribute({Name, Term}) ->
erl_syntax:attribute(erl_syntax:atom(Name),
[erl_syntax:abstract(Term)]).
is_auto_import({F, A}) ->
erl_internal:bif(F, A).
timestamp() ->
{{Yr, Mth, Dy}, {Hr, Mt, Sc}} = erlang:localtime(),
lists:flatten(io_lib:fwrite("Created by Igor "
"~w-~2.2.0w-~2.2.0w, "
"~2.2.0w:~2.2.0w:~2.2.0w.",
[Yr, Mth, Dy, Hr, Mt, Sc])).
filename([C | T]) when is_integer(C), C > 0, C =< 255 ->
[C | filename(T)];
filename([H|T]) ->
filename(H) ++ filename(T);
filename([]) ->
[];
filename(N) when is_atom(N) ->
atom_to_list(N);
filename(N) ->
report_error("bad filename: `~P'.", [N, 25]),
exit(error).
duplicates(Xs) ->
ordsets:from_list(Xs -- ordsets:from_list(Xs)).
split_list(F, L) ->
split_list(L, F, [], []).
split_list([H | T], F, A1, A2) ->
case F(H) of
true ->
split_list(T, F, [H | A1], A2);
false ->
split_list(T, F, A1, [H | A2])
end;
split_list([], _, A1, A2) ->
{lists:reverse(A1), lists:reverse(A2)}.
rewrite(Source, Target) ->
erl_syntax:copy_attrs(Source, Target).
comment_note([L | Ls]) ->
comment([?NOTE_HEADER ++ L | Ls], ?NOTE_PREFIX).
comment(Txt) ->
comment(Txt, ?COMMENT_PREFIX).
comment(Txt, Prefix) ->
erl_syntax:comment(prefix_lines(split_lines(Txt), Prefix)).
prefix_lines([L | Ls], Prefix) ->
[Prefix ++ L | prefix_lines(Ls, Prefix)];
prefix_lines([], _) ->
[].
split_lines(Ls) ->
split_lines(Ls, []).
split_lines([L | Ls], Ls1) ->
split_lines(Ls, split_lines(L, [], Ls1));
split_lines([], Ls1) ->
lists:reverse(Ls1).
split_lines([$\r, $\n | Cs], Cs1, Ls) ->
split_lines_1(Cs, Cs1, Ls);
split_lines([$\r | Cs], Cs1, Ls) ->
split_lines_1(Cs, Cs1, Ls);
split_lines([$\n | Cs], Cs1, Ls) ->
split_lines_1(Cs, Cs1, Ls);
split_lines([C | Cs], Cs1, Ls) ->
split_lines(Cs, [C | Cs1], Ls);
split_lines([], Cs, Ls) ->
[lists:reverse(Cs) | Ls].
split_lines_1(Cs, Cs1, Ls) ->
split_lines(Cs, [], [lists:reverse(Cs1) | Ls]).
%% =====================================================================
%% Reporting
warning_unsafe_call(Name, Module, Target) ->
report_warning("call to `~w' in module `~w' "
"possibly unsafe in `~s'.", [Name, Module, Target]).
warning_apply_2(Module, Target) ->
report_warning("call to `apply/2' in module `~w' "
"possibly unsafe in `~s'.", [Module, Target]).
error_open_output(Name) ->
report_error("cannot open file `~s' for output.", [filename(Name)]).
error_read_file(Name) ->
report_error("error reading file `~s'.", [filename(Name)]).
error_read_file_info(Name) ->
report_error("error getting file info: `~s'.", [filename(Name)]).
error_write_file(Name) ->
report_error("error writing to file `~s'.", [filename(Name)]).
error_backup_file(Name) ->
report_error("could not create backup of file `~s'.",
[filename(Name)]).
verbose(S, Opts) ->
verbose(S, [], Opts).
verbose(S, Vs, Opts) ->
case proplists:get_bool(verbose, Opts) of
true ->
report(S, Vs);
false ->
ok
end.
report_error(S) ->
report_error(S, []).
report_error(S, Vs) ->
report(S, Vs).
report_warning(S) ->
report_warning(S, []).
report_warning(S, Vs) ->
report("warning: " ++ S, Vs).
% report(S) ->
% report(S, []).
report(S, Vs) ->
io:fwrite(lists:concat([?MODULE, ": ", S, "\n"]), Vs).