%% ===================================================================== %% 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 %% %% @copyright 2001-2007 Richard Carlsson %% @author Richard Carlsson %% @version {@version} %% @end %% ===================================================================== %% TODO: check weirdness in name generation for @spec f(TypeName, ...) -> ... %% TODO: option for ignoring functions matching some pattern ('..._test_'/0) %% TODO: @private_type tag, opaque unless generating private docs? %% TODO: document the record type syntax %% TODO: some 'skip' option for ignoring particular modules/packages? %% TODO: intermediate-level packages: document even if no local sources. %% TODO: multiline comment support (needs modified comment representation) %% TODO: config-file for default settings %% TODO: config: locations of all local docdirs; generate local doc-index page %% TODO: config: URL:s of offline packages/apps %% TODO: config: default stylesheet %% TODO: config: default header/footer, etc. %% TODO: offline linkage %% TODO: including source code, explicitly and/or automatically %% @doc EDoc - the Erlang program documentation generator. %% %% This module provides the main user interface to EDoc. %% -module(edoc). -export([packages/1, packages/2, files/1, files/2, application/1, application/2, application/3, toc/1, toc/2, toc/3, run/3, file/1, file/2, read/1, read/2, layout/1, layout/2, get_doc/1, get_doc/2, get_doc/3, read_comments/1, read_comments/2, read_source/1, read_source/2]). -compile({no_auto_import,[error/1]}). -include("edoc.hrl"). %% @spec (Name::filename()) -> ok %% @equiv file(Name, []) %% @deprecated See {@link file/2} for details. file(Name) -> file(Name, []). %% @spec file(filename(), proplist()) -> ok %% %% @type filename() = //kernel/file:filename() %% @type proplist() = [term()] %% %% @deprecated This is part of the old interface to EDoc and is mainly %% kept for backwards compatibility. The preferred way of generating %% documentation is through one of the functions {@link application/2}, %% {@link packages/2} and {@link files/2}. %% %% @doc Reads a source code file and outputs formatted documentation to %% a corresponding file. %% %% Options: %%
%%
{@type {dir, filename()@}} %%
%%
Specifies the output directory for the created file. (By %% default, the output is written to the directory of the source %% file.) %%
%%
{@type {source_suffix, string()@}} %%
%%
Specifies the expected suffix of the input file. The default %% value is `".erl"'. %%
%%
{@type {file_suffix, string()@}} %%
%%
Specifies the suffix for the created file. The default value is %% `".html"'. %%
%%
%% %% See {@link get_doc/2} and {@link layout/2} for further %% options. %% %% For running EDoc from a Makefile or similar, see %% {@link edoc_run:file/1}. %% %% @see read/2 %% NEW-OPTIONS: source_suffix, file_suffix, dir %% INHERIT-OPTIONS: read/2 file(Name, Options) -> Text = read(Name, Options), SrcSuffix = proplists:get_value(source_suffix, Options, ?DEFAULT_SOURCE_SUFFIX), BaseName = filename:basename(Name, SrcSuffix), Suffix = proplists:get_value(file_suffix, Options, ?DEFAULT_FILE_SUFFIX), Dir = proplists:get_value(dir, Options, filename:dirname(Name)), Encoding = [{encoding, edoc_lib:read_encoding(Name, [])}], edoc_lib:write_file(Text, Dir, BaseName ++ Suffix, '', Encoding). %% TODO: better documentation of files/1/2, packages/1/2, application/1/2/3 %% @spec (Files::[filename() | {package(), [filename()]}]) -> ok %% @equiv packages(Packages, []) files(Files) -> files(Files, []). %% @spec (Files::[filename() | {package(), [filename()]}], %% Options::proplist()) -> ok %% @doc Runs EDoc on a given set of source files. See {@link run/3} for %% details, including options. %% @equiv run([], Files, Options) files(Files, Options) -> run([], Files, Options). %% @spec (Packages::[package()]) -> ok %% @equiv packages(Packages, []) packages(Packages) -> packages(Packages, []). %% @spec (Packages::[package()], Options::proplist()) -> ok %% @type package() = atom() | string() %% %% @doc Runs EDoc on a set of packages. The `source_path' option is used %% to locate the files; see {@link run/3} for details, including %% options. This function automatically appends the current directory to %% the source path. %% %% @equiv run(Packages, [], Options) packages(Packages, Options) -> run(Packages, [], Options ++ [{source_path, [?CURRENT_DIR]}]). %% @spec (Application::atom()) -> ok %% @equiv application(Application, []) application(App) -> application(App, []). %% @spec (Application::atom(), Options::proplist()) -> ok %% @doc Run EDoc on an application in its default app-directory. See %% {@link application/3} for details. %% @see application/1 application(App, Options) when is_atom(App) -> case code:lib_dir(App) of Dir when is_list(Dir) -> application(App, Dir, Options); _ -> edoc_report:report("cannot find application directory for '~s'.", [App]), exit(error) end. %% @spec (Application::atom(), Dir::filename(), Options::proplist()) %% -> ok %% @doc Run EDoc on an application located in the specified directory. %% Tries to automatically set up good defaults. Unless the user %% specifies otherwise: %% %% %% See {@link run/3} for details, including options. %% %% @see application/2 application(App, Dir, Options) when is_atom(App) -> Src = edoc_lib:try_subdir(Dir, ?SOURCE_DIR), Overview = filename:join(edoc_lib:try_subdir(Dir, ?EDOC_DIR), ?OVERVIEW_FILE), Opts = Options ++ [{source_path, [Src]}, subpackages, {title, io_lib:fwrite("The ~s application", [App])}, {overview, Overview}, {dir, filename:join(Dir, ?EDOC_DIR)}, {includes, [filename:join(Dir, "include")]}], Opts1 = set_app_default(App, Dir, Opts), %% Recursively document all subpackages of '' - i.e., everything. run([''], [], [{application, App} | Opts1]). %% Try to set up a default application base URI in a smart way if the %% user has not specified it explicitly. set_app_default(App, Dir0, Opts) -> case proplists:get_value(app_default, Opts) of undefined -> AppName = atom_to_list(App), Dir = edoc_lib:simplify_path(filename:absname(Dir0)), AppDir = case filename:basename(Dir) of AppName -> filename:dirname(Dir); _ -> ?APP_DEFAULT end, [{app_default, AppDir} | Opts]; _ -> Opts end. %% If no source files are found for a (specified) package, no package %% documentation will be generated either (even if there is a %% package-documentation file). This is the way it should be. For %% specified files, use empty package (unless otherwise specified). The %% assumed package is always used for creating the output. If the actual %% module or package of the source differs from the assumption gathered %% from the path and file name, a warning should be issued (since links %% are likely to be incorrect). opt_defaults() -> [packages]. opt_negations() -> [{no_preprocess, preprocess}, {no_subpackages, subpackages}, {no_report_missing_types, report_missing_types}, {no_packages, packages}]. %% @spec run(Packages::[package()], %% Files::[filename() | {package(), [filename()]}], %% Options::proplist()) -> ok %% @doc Runs EDoc on a given set of source files and/or packages. Note %% that the doclet plugin module has its own particular options; see the %% `doclet' option below. %% %% Also see {@link layout/2} for layout-related options, and %% {@link get_doc/2} for options related to reading source %% files. %% %% Options: %%
%%
{@type {app_default, string()@}} %%
%%
Specifies the default base URI for unknown applications. %%
%%
{@type {application, App::atom()@}} %%
%%
Specifies that the generated documentation describes the %% application `App'. This mainly affects generated references. %%
%%
{@type {dir, filename()@}} %%
%%
Specifies the target directory for the generated documentation. %%
%%
{@type {doc_path, [string()]@}} %%
%%
Specifies a list of URI:s pointing to directories that contain %% EDoc-generated documentation. URI without a `scheme://' part are %% taken as relative to `file://'. (Note that such paths must use %% `/' as separator, regardless of the host operating system.) %%
%%
{@type {doclet, Module::atom()@}} %%
%%
Specifies a callback module to be used for creating the %% documentation. The module must export a function `run(Cmd, Ctxt)'. %% The default doclet module is {@link edoc_doclet}; see {@link %% edoc_doclet:run/2} for doclet-specific options. %%
%%
{@type {exclude_packages, [package()]@}} %%
%%
Lists packages to be excluded from the documentation. Typically %% used in conjunction with the `subpackages' option. %%
%%
{@type {file_suffix, string()@}} %%
%%
Specifies the suffix used for output files. The default value is %% `".html"'. Note that this also affects generated references. %%
%%
{@type {new, boolean()@}} %%
%%
If the value is `true', any existing `edoc-info' file in the %% target directory will be ignored and overwritten. The default %% value is `false'. %%
%%
{@type {packages, boolean()@}} %%
%%
If the value is `true', it it assumed that packages (module %% namespaces) are being used, and that the source code directory %% structure reflects this. The default value is `true'. (Usually, %% this does the right thing even if all the modules belong to the %% top-level "empty" package.) `no_packages' is an alias for %% `{packages, false}'. See the `subpackages' option below for %% further details. %% %% If the source code is organized in a hierarchy of %% subdirectories although it does not use packages, use %% `no_packages' together with the recursive-search `subpackages' %% option (on by default) to automatically generate documentation %% for all the modules. %%
%%
{@type {source_path, [filename()]@}} %%
%%
Specifies a list of file system paths used to locate the source %% code for packages. %%
%%
{@type {source_suffix, string()@}} %%
%%
Specifies the expected suffix of input files. The default %% value is `".erl"'. %%
%%
{@type {subpackages, boolean()@}} %%
%%
If the value is `true', all subpackages of specified packages %% will also be included in the documentation. The default value is %% `false'. `no_subpackages' is an alias for `{subpackages, %% false}'. See also the `exclude_packages' option. %% %% Subpackage source files are found by recursively searching %% for source code files in subdirectories of the known source code %% root directories. (Also see the `source_path' option.) Directory %% names must begin with a lowercase letter and contain only %% alphanumeric characters and underscore, or they will be ignored. %% (For example, a subdirectory named `test-files' will not be %% searched.) %%
%%
%% %% @see files/2 %% @see packages/2 %% @see application/2 %% NEW-OPTIONS: source_path, application %% INHERIT-OPTIONS: init_context/1 %% INHERIT-OPTIONS: expand_sources/2 %% INHERIT-OPTIONS: target_dir_info/5 %% INHERIT-OPTIONS: edoc_lib:find_sources/3 %% INHERIT-OPTIONS: edoc_lib:run_doclet/2 %% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 run(Packages, Files, Opts0) -> Opts = expand_opts(Opts0), Ctxt = init_context(Opts), Dir = Ctxt#context.dir, Path = proplists:append_values(source_path, Opts), Ss = sources(Path, Packages, Opts), {Ss1, Ms} = expand_sources(expand_files(Files) ++ Ss, Opts), Ps = [P || {_, P, _, _} <- Ss1], App = proplists:get_value(application, Opts, ?NO_APP), {App1, Ps1, Ms1} = target_dir_info(Dir, App, Ps, Ms, Opts), %% The "empty package" is never included in the list of packages. Ps2 = edoc_lib:unique(lists:sort(Ps1)) -- [''], Ms2 = edoc_lib:unique(lists:sort(Ms1)), Fs = package_files(Path, Ps2), Env = edoc_lib:get_doc_env(App1, Ps2, Ms2, Opts), Ctxt1 = Ctxt#context{env = Env}, Cmd = #doclet_gen{sources = Ss1, app = App1, packages = Ps2, modules = Ms2, filemap = Fs }, F = fun (M) -> M:run(Cmd, Ctxt1) end, edoc_lib:run_doclet(F, Opts). expand_opts(Opts0) -> proplists:substitute_negations(opt_negations(), Opts0 ++ opt_defaults()). %% NEW-OPTIONS: dir %% DEFER-OPTIONS: run/3 init_context(Opts) -> #context{dir = proplists:get_value(dir, Opts, ?CURRENT_DIR), opts = Opts }. %% INHERIT-OPTIONS: edoc_lib:find_sources/3 sources(Path, Packages, Opts) -> lists:foldl(fun (P, Xs) -> edoc_lib:find_sources(Path, P, Opts) ++ Xs end, [], Packages). package_files(Path, Packages) -> Name = ?PACKAGE_FILE, % this is hard-coded for now D = lists:foldl(fun (P, D) -> F = edoc_lib:find_file(Path, P, Name), dict:store(P, F, D) end, dict:new(), Packages), fun (P) -> case dict:find(P, D) of {ok, F} -> F; error -> "" end end. %% Expand user-specified sets of files. expand_files([{P, Fs1} | Fs]) -> [{P, filename:basename(F), filename:dirname(F)} || F <- Fs1] ++ expand_files(Fs); expand_files([F | Fs]) -> [{'', filename:basename(F), filename:dirname(F)} | expand_files(Fs)]; expand_files([]) -> []. %% Create the (assumed) full module names. Keep only the first source %% for each module, but preserve the order of the list. %% NEW-OPTIONS: source_suffix, packages %% DEFER-OPTIONS: run/3 expand_sources(Ss, Opts) -> Suffix = proplists:get_value(source_suffix, Opts, ?DEFAULT_SOURCE_SUFFIX), Ss1 = case proplists:get_bool(packages, Opts) of true -> Ss; false -> [{'',F,D} || {_P,F,D} <- Ss] end, expand_sources(Ss1, Suffix, sets:new(), [], []). expand_sources([{'', F, D} | Fs], Suffix, S, As, Ms) -> M = list_to_atom(filename:rootname(F, Suffix)), case sets:is_element(M, S) of true -> expand_sources(Fs, Suffix, S, As, Ms); false -> S1 = sets:add_element(M, S), expand_sources(Fs, Suffix, S1, [{M, '', F, D} | As], [M | Ms]) end; expand_sources([], _Suffix, _S, As, Ms) -> {lists:reverse(As), lists:reverse(Ms)}. %% NEW-OPTIONS: new target_dir_info(Dir, App, Ps, Ms, Opts) -> case proplists:get_bool(new, Opts) of true -> {App, Ps, Ms}; false -> {App1, Ps1, Ms1} = edoc_lib:read_info_file(Dir), {if App == ?NO_APP -> App1; true -> App end, Ps ++ Ps1, Ms ++ Ms1} end. %% @hidden Not official yet toc(Dir) -> toc(Dir, []). %% @equiv toc(Dir, Paths, []) %% @hidden Not official yet %% NEW-OPTIONS: doc_path toc(Dir, Opts) -> Paths = proplists:append_values(doc_path, Opts) ++ edoc_lib:find_doc_dirs(), toc(Dir, Paths, Opts). %% @doc Create a meta-level table of contents. %% @hidden Not official yet %% INHERIT-OPTIONS: init_context/1 %% INHERIT-OPTIONS: edoc_lib:run_doclet/2 %% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 toc(Dir, Paths, Opts0) -> Opts = expand_opts(Opts0 ++ [{dir, Dir}]), Ctxt = init_context(Opts), Env = edoc_lib:get_doc_env('', [], [], Opts), Ctxt1 = Ctxt#context{env = Env}, F = fun (M) -> M:run(#doclet_toc{paths=Paths}, Ctxt1) end, edoc_lib:run_doclet(F, Opts). %% @spec read(File::filename()) -> string() %% @equiv read(File, []) read(File) -> read(File, []). %% @spec read(File::filename(), Options::proplist()) -> string() %% %% @doc Reads and processes a source file and returns the resulting %% EDoc-text as a string. See {@link get_doc/2} and {@link layout/2} for %% options. %% %% @see file/2 %% INHERIT-OPTIONS: get_doc/2, layout/2 read(File, Opts) -> {_ModuleName, Doc} = get_doc(File, Opts), layout(Doc, Opts). %% @spec layout(Doc::edoc_module()) -> string() %% @equiv layout(Doc, []) layout(Doc) -> layout(Doc, []). %% @spec layout(Doc::edoc_module(), Options::proplist()) -> string() %% %% @doc Transforms EDoc module documentation data to text. The default %% layout creates an HTML document. %% %% Options: %%
%%
{@type {layout, Module::atom()@}} %%
%%
Specifies a callback module to be used for formatting. The %% module must export a function `module(Doc, Options)'. The %% default callback module is {@link edoc_layout}; see {@link %% edoc_layout:module/2} for layout-specific options. %%
%%
%% %% @see layout/1 %% @see run/3 %% @see read/2 %% @see file/2 %% INHERIT-OPTIONS: edoc_lib:run_layout/2 layout(Doc, Opts) -> F = fun (M) -> M:module(Doc, Opts) end, edoc_lib:run_layout(F, Opts). %% @spec (File) -> [comment()] %% @type comment() = {Line, Column, Indentation, Text} %% where %% Line = integer(), %% Column = integer(), %% Indentation = integer(), %% Text = [string()] %% @equiv read_comments(File, []) read_comments(File) -> read_comments(File, []). %% @spec read_comments(File::filename(), Options::proplist()) -> %% [comment()] %% %% @doc Extracts comments from an Erlang source code file. See the %% module {@link //syntax_tools/erl_comment_scan} for details on the %% representation of comments. Currently, no options are avaliable. read_comments(File, _Opts) -> erl_comment_scan:file(File). %% @spec (File) -> [syntaxTree()] %% @equiv read_source(File, []) read_source(Name) -> read_source(Name, []). %% @spec read_source(File::filename(), Options::proplist()) -> %% [syntaxTree()] %% %% @type syntaxTree() = //syntax_tools/erl_syntax:syntaxTree() %% %% @doc Reads an Erlang source file and returns the list of "source code %% form" syntax trees. %% %% Options: %%
%%
{@type {preprocess, boolean()@}} %%
%%
If the value is `true', the source file will be read via the %% Erlang preprocessor (`epp'). The default value is `false'. %% `no_preprocess' is an alias for `{preprocess, false}'. %% %% Normally, preprocessing is not necessary for EDoc to work, but %% if a file contains too exotic definitions or uses of macros, it %% will not be possible to read it without preprocessing. Note: %% comments in included files will not be available to EDoc, even %% with this option enabled. %%
%%
{@type {includes, Path::[string()]@}} %%
%%
Specifies a list of directory names to be searched for include %% files, if the `preprocess' option is turned on. Also used with %% the `@headerfile' tag. The default value is the empty list. The %% directory of the source file is always automatically appended to %% the search path. %%
%%
{@type {macros, [{atom(), term()@}]@}} %%
%%
Specifies a list of pre-defined Erlang preprocessor (`epp') %% macro definitions, used if the `preprocess' option is turned on. %% The default value is the empty list.
%%
%%
{@type {report_missing_types, boolean()@}} %%
%%
If the value is `true', warnings are issued for missing types. %% The default value is `false'. %% `no_report_missing_types' is an alias for %% `{report_missing_types, false}'. %%
%% %% @see get_doc/2 %% @see //syntax_tools/erl_syntax %% NEW-OPTIONS: [no_]preprocess (preprocess -> includes, macros) read_source(Name, Opts0) -> Opts = expand_opts(Opts0), case read_source_1(Name, Opts) of {ok, Forms} -> check_forms(Forms, Name), Forms; {error, R} -> edoc_report:error({"error reading file '~s'.", [edoc_lib:filename(Name)]}), exit({error, R}) end. read_source_1(Name, Opts) -> case proplists:get_bool(preprocess, Opts) of true -> read_source_2(Name, Opts); false -> epp_dodger:quick_parse_file(Name, Opts ++ [{no_fail, false}]) end. read_source_2(Name, Opts) -> Includes = proplists:append_values(includes, Opts) ++ [filename:dirname(Name)], Macros = proplists:append_values(macros, Opts), %% epp:parse_file(Name, Includes, Macros). parse_file(Name, Includes, Macros). %% The code below has been copied from epp.erl. %% %% Copy the line of the last token to the last token that will be %% part of the parse tree. %% %% The last line is used in edoc_extract:find_type_docs() to determine %% if a type declaration is followed by a comment. %% %% -type t() :: [ %% {tag, integer()} %% ]. %% %% Protocol options. %% %% The line of the dot token will be copied to the integer token. parse_file(Name, Includes, Macros) -> case epp:open(Name, Includes, Macros) of {ok, Epp} -> try {ok, parse_file(Epp)} after _ = epp:close(Epp) end; Error -> Error end. parse_file(Epp) -> case scan_and_parse(Epp) of {ok, Form} -> case Form of {attribute,La,record,{Record, Fields}} -> case epp:normalize_typed_record_fields(Fields) of {typed, NewFields} -> [{attribute, La, record, {Record, NewFields}}, {attribute, La, type, {{record, Record}, Fields, []}} | parse_file(Epp)]; not_typed -> [Form | parse_file(Epp)] end; _ -> [Form | parse_file(Epp)] end; {error, E} -> [{error, E} | parse_file(Epp)]; {eof, Location} -> [{eof, Location}] end. scan_and_parse(Epp) -> case epp:scan_erl_form(Epp) of {ok, Toks0} -> Toks = fix_last_line(Toks0), case erl_parse:parse_form(Toks) of {ok, Form} -> {ok, Form}; Else -> Else end; Else -> Else end. fix_last_line(Toks0) -> Toks1 = lists:reverse(Toks0), {line, LastLine} = erl_scan:token_info(hd(Toks1), line), fll(Toks1, LastLine, []). fll([{Category, Attributes0, Symbol} | L], LastLine, Ts) -> F = fun(_OldLine) -> LastLine end, Attributes = erl_scan:set_attribute(line, Attributes0, F), lists:reverse(L, [{Category, Attributes, Symbol} | Ts]); fll([T | L], LastLine, Ts) -> fll(L, LastLine, [T | Ts]); fll(L, _LastLine, Ts) -> lists:reverse(L, Ts). check_forms(Fs, Name) -> Fun = fun (F) -> case erl_syntax:type(F) of error_marker -> case erl_syntax:error_marker_info(F) of {L, M, D} -> edoc_report:error(L, Name, {format_error, M, D}); Other -> edoc_report:report(Name, "unknown error in " "source code: ~w.", [Other]) end, exit(error); _ -> ok end end, lists:foreach(Fun, Fs). %% @spec get_doc(File::filename()) -> {ModuleName, edoc_module()} %% @equiv get_doc(File, []) get_doc(File) -> get_doc(File, []). %% @spec get_doc(File::filename(), Options::proplist()) -> %% {ModuleName, edoc_module()} %% ModuleName = atom() %% %% @type edoc_module(). The EDoc documentation data for a module, %% expressed as an XML document in {@link //xmerl. XMerL} format. See %% the file `edoc.dtd' for details. %% %% @doc Reads a source code file and extracts EDoc documentation data. %% Note that without an environment parameter (see {@link get_doc/3}), %% hypertext links may not be correct. %% %% Options: %%
%%
{@type {def, Macros@}} %%
%%
    %%
  • `Macros' = {@type Macro | [Macro]}
  • %%
  • `Macro' = {@type {Name::atom(), Text::string()@}}
  • %%
%% Specifies a set of EDoc macro definitions. See %% Inline macro expansion %% for details. %%
%%
{@type {hidden, boolean()@}} %%
%%
If the value is `true', documentation of hidden functions will %% also be included. The default value is `false'. %%
%%
{@type {private, boolean()@}} %%
%%
If the value is `true', documentation of private functions will %% also be included. The default value is `false'. %%
%%
{@type {todo, boolean()@}} %%
%%
If the value is `true', To-Do notes written using `@todo' or %% `@TODO' tags will be included in the documentation. The default %% value is `false'. %%
%%
%% %% See {@link read_source/2}, {@link read_comments/2} and {@link %% edoc_lib:get_doc_env/4} for further options. %% %% @see get_doc/3 %% @see run/3 %% @see edoc_extract:source/5 %% @see read/2 %% @see layout/2 %% INHERIT-OPTIONS: get_doc/3 %% INHERIT-OPTIONS: edoc_lib:get_doc_env/4 get_doc(File, Opts) -> Env = edoc_lib:get_doc_env(Opts), get_doc(File, Env, Opts). %% @spec get_doc(File::filename(), Env::edoc_lib:edoc_env(), %% Options::proplist()) -> {ModuleName, edoc_module()} %% ModuleName = atom() %% %% @doc Like {@link get_doc/2}, but for a given environment %% parameter. `Env' is an environment created by {@link %% edoc_lib:get_doc_env/4}. %% INHERIT-OPTIONS: read_source/2, read_comments/2, edoc_extract:source/5 %% DEFER-OPTIONS: get_doc/2 get_doc(File, Env, Opts) -> edoc_extract:source(File, Env, Opts).