How to attach comments to forms when they are spaced by line breaks?

For example:

-module(foo).
-export([foo/0, bar/0]).

%% @doc I'm attached to a function :)
foo() -> foo.

%% @doc I'm not because of the line break :(

bar() -> bar.

Running:

1> {source, File} = proplists:lookup(source, foo:module_info(compile)).
{source,"/home/williamthome/Projects/williamthome/doctest/src/foo.erl"}
2> {ok, Forms} = epp:parse_file(File, [], []).
{ok,[{attribute,1,file,
                {"/home/williamthome/Projects/williamthome/doctest/src/foo.erl",
                 1}},
     {attribute,1,module,foo},
     {attribute,2,export,[{foo,0},{bar,0}]},
     {function,5,foo,0,[{clause,5,[],[],[{atom,5,foo}]}]},
     {function,9,bar,0,[{clause,9,[],[],[{atom,9,bar}]}]},
     {eof,10}]}
3> Comments = erl_comment_scan:file(File).
[{4,1,0,["% @doc I'm attached to a function :)"]},
 {7,1,0,["% @doc I'm not because of the line break :("]}]
4>  erl_syntax:form_list_elements(erl_syntax:flatten_form_list(
..   erl_recomment:recomment_forms(Forms, Comments))).
%% suppressed for clarify
 {tree,function,
     {attr,5,[],
         {com,
             [{tree,comment, % <- The comment is attached to the function
                  {attr,4,[],none},
                  {comment,0,["% @doc I'm attached to a function :)"]}}],
             []}},
     {func,
         {tree,atom,{attr,5,[],none},foo},
         [{tree,clause,
              {attr,5,[],none},
              {clause,[],none,[{atom,5,foo}]}}]}},
 {tree,comment, % <- The comment is not attached to the function
     {attr,7,[],none},
     {comment,0,["% @doc I'm not because of the line break :("]}},
 {tree,function,
     {attr,9,[],none},
     {func,
         {tree,atom,{attr,9,[],none},bar},
         [{tree,clause,
              {attr,9,[],none},
              {clause,[],none,[{atom,9,bar}]}}]}},
 {eof,10}]

As you can see, only the first @doc is attached to the function.

Is there a function to attach comments to functions even if separated by line breaks? Or do I need to do it manually? I’m not finding where EDoc resolves something like this (or if it does).

CC @garazdawi

1 Like

If you’re fine with a workaround, using the new doc attribute works in OTP 27.

-module(foo).
-export([foo/0, bar/0]).

-doc "I'm attached to a function :)".
foo() -> foo.

-doc "I am as well, in spite of the line break :?)".

bar() -> bar.

I’ve added support for EDoc tags to Doctest, for example:

-module(foo).
-export([foo/0, bar/0]).

%% @doc I'm attached to a function :)
%% ```
%% 1> foo().
%% bar
%% '''
foo() -> foo.

%% @doc I'm not because of the line break :(
%% ```
%% 1> bar().
%% baz
%% '''

bar() -> bar.

Testing:

$ rebar3 shell
1> doctest:module(foo).
 FAIL  ./src/foo.erl:6 @doc

   ❌ assertEqual

   Expected: bar
   Received: foo

   β”‚
 6 β”‚ %% 1> foo().
 7 β”‚ %% bar
   β”‚
   └── at ./src/foo.erl:6



Tests: 1 failed, 1 total
 Time: 0.005 seconds

Only the first function is tested. But, with the line break removed:

$ rebar3 shell
1> doctest:module(foo).
 FAIL  ./src/foo.erl:6 @doc

   ❌ assertEqual

   Expected: bar
   Received: foo

   β”‚
 6 β”‚ %% 1> foo().
 7 β”‚ %% bar
   β”‚
   └── at ./src/foo.erl:6

 FAIL  ./src/foo.erl:13 @doc

    ❌ assertEqual

    Expected: baz
    Received: bar

    β”‚
 13 β”‚ %% 1> bar().
 14 β”‚ %% baz
    β”‚
    └── at ./src/foo.erl:13



Tests: 2 failed, 2 total
 Time: 0.007 seconds

Here is where EDoc code blocks are extracted: doctest/src/doctest_extract_tag.erl at 1e4bbfd17263df1c3f6e2da033a43fb452394dc4 Β· williamthome/doctest Β· GitHub

If I remember correctly, line breaks are allowed between docs and functions.

Ok, found it, edoc_extract:source/3 is what I need:

1> rp(element(3, edoc_extract:source("/home/williamthome/Projects/williamthome/doctest/src/foo.erl", edoc_lib:get_doc_env([]), [return_entries]))).
[{entry,module,[],1,undefined,[]},
 {entry,footer,[],0,undefined,[]},
 {entry,{foo,0},
        [],9,true,
        [{tag,doc,4,comment,
              [{xmlText,[{doc,1}],
                        1,[],"I'm attached to a function :)\n  ",text},
               {xmlElement,pre,pre,[],
                           {xmlNamespace,[],[]},
                           [{doc,1}],
                           2,[],
                           [{xmlText,[{pre,2},{doc,1}],
                                     1,[],"  1> foo().\n  bar",cdata}],
                           [],
                           "/home/williamthome/Projects/williamthome/doctest",
                           undeclared}],
              undefined}]},
 {entry,{bar,0},
        [],17,true,
        [{tag,doc,11,comment,
              [{xmlText,[{doc,1}],
                        1,[],"I'm not because of the line break :(\n  ",text},
               {xmlElement,pre,pre,[],
                           {xmlNamespace,[],[]},
                           [{doc,1}],
                           2,[],
                           [{xmlText,[{pre,2},{doc,1}],
                                     1,[],"  1> bar().\n  baz",cdata}],
                           [],
                           "/home/williamthome/Projects/williamthome/doctest",
                           undeclared}],
              undefined}]}]